From 20eae8af322b147590c56738dc70d24397e8b1f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 16:49:59 +0100 Subject: [PATCH 01/20] base implementation for activities graphql --- ayon_api/constants.py | 10 +++++ ayon_api/graphql_queries.py | 44 +++++++++++++++++++++ ayon_api/server_api.py | 76 ++++++++++++++++++++++++++++++++++++- 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 594155706..99a6247f2 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -184,3 +184,13 @@ "description", "author", } + +DEFAULT_ACTIVITY_FIELDS = { + "activityId", + "activityType", + "activityData", + "body", + "entityId", + "entityType", + "author.name", +} diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 8363d1ece..bc10dc6d8 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -636,3 +636,47 @@ def users_graphql_query(fields): for k, v in value.items(): query_queue.append((k, v, field)) return query + + +def activities_graphql_query(fields): + query = GraphQlQuery("Activities") + project_name_var = query.add_variable("projectName", "String!") + activity_ids_var = query.add_variable("activityIds", "[String]") + activity_types_var = query.add_variable("activityTypes", "[String]") + entity_ids_var = query.add_variable("entityIds", "[String]") + entity_names_var = query.add_variable("entityNames", "[String]") + entity_type_var = query.add_variable("entityType", "String!") + changed_after_var = query.add_variable("changedAfter", "String!") + changed_before_var = query.add_variable("changedBefore", "String!") + reference_types_var = query.add_variable("referenceTypes", "String!") + + project_field = query.add_field_with_edges("project") + project_field.set_filter("name", project_name_var) + + activities_field = project_field.add_field_with_edges("activities") + activities_field.set_filter("activityIds", activity_ids_var) + activities_field.set_filter("activityTypes", activity_types_var) + activities_field.set_filter("entityIds", entity_ids_var) + activities_field.set_filter("entityNames", entity_names_var) + activities_field.set_filter("entityType", entity_type_var) + activities_field.set_filter("changedAfter", changed_after_var) + activities_field.set_filter("changedBefore", changed_before_var) + activities_field.set_filter("referenceTypes", reference_types_var) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, activities_field)) + + while query_queue: + item = query_queue.popleft() + key, value, parent = item + field = parent.add_field(key) + if value is FIELD_VALUE: + continue + + for k, v in value.items(): + query_queue.append((k, v, field)) + + return query diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 30da35ac2..77222f227 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -16,7 +16,7 @@ import warnings import itertools from contextlib import contextmanager -from typing import Optional +from typing import Optional, Iterable try: from http import HTTPStatus @@ -49,6 +49,7 @@ REPRESENTATION_FILES_FIELDS, DEFAULT_WORKFILE_INFO_FIELDS, DEFAULT_EVENT_FIELDS, + DEFAULT_ACTIVITY_FIELDS, DEFAULT_USER_FIELDS, DEFAULT_LINK_FIELDS, ) @@ -68,6 +69,7 @@ workfiles_info_graphql_query, events_graphql_query, users_graphql_query, + activities_graphql_query, ) from .exceptions import ( FailedOperations, @@ -1729,6 +1731,75 @@ def enroll_event_job( return response.data + def get_activities( + self, + project_name: str, + activity_ids: Optional[Iterable[str]] = None, + activity_types: Optional[Iterable[str]] = None, + entity_ids: Optional[Iterable[str]] = None, + entity_names: Optional[Iterable[str]] = None, + entity_type: Optional[str] = None, + changed_after: Optional[str] = None, + changed_before: Optional[str] = None, + reference_types: Optional[Iterable[str]] = None, + fields: Optional[Iterable[str]] = None, + ): + """Get activities from server with filtering options. + + Args: + project_name (str): Project on which event happened. + activity_ids (Optional[Iterable[str]]): Activity ids. + activity_types (Optional[Iterable[str]]): Activity types. + entity_ids (Optional[Iterable[str]]): Entity ids. + entity_names (Optional[Iterable[str]]): Entity names. + entity_type (Optional[str]): Entity type. + changed_after (Optional[str]): Return only activities changed + after given iso datetime string. + changed_before (Optional[str]): Return only activities changed + before given iso datetime string. + reference_types (Optional[Iterable[str]]): Reference types. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + + Returns: + Generator[dict[str, Any]]: Available activities matching filters. + + """ + if not project_name: + return + filters = { + "projectName": project_name, + } + + if not _prepare_list_filters( + filters, + ("activityIds", activity_ids), + ("activityTypes", activity_types), + ("entityIds", entity_ids), + ("entityNames", entity_names), + ("referenceTypes", reference_types), + ): + return + + for filter_key, filter_value in ( + ("entityType", entity_type), + ("changedAfter", changed_after), + ("changedBefore", changed_before), + ): + if filter_value is not None: + filters[filter_key] = filter_value + + if not fields: + fields = self.get_default_fields_for_type("activity") + + query = activities_graphql_query(set(fields)) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for event in parsed_data["activities"]: + yield event + def _endpoint_to_url( self, endpoint: str, @@ -2306,6 +2377,9 @@ def get_default_fields_for_type(self, entity_type): if entity_type == "event": return set(DEFAULT_EVENT_FIELDS) + if entity_type == "activity": + return set(DEFAULT_ACTIVITY_FIELDS) + if entity_type == "project": entity_type_defaults = set(DEFAULT_PROJECT_FIELDS) if not self.graphql_allows_data_in_query: From db3680d4b82041e0e8f1b74bd53a25e4a18d3efd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:02:05 +0100 Subject: [PATCH 02/20] small fixes --- ayon_api/graphql_queries.py | 2 +- ayon_api/server_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index bc10dc6d8..e44cd07bc 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -650,7 +650,7 @@ def activities_graphql_query(fields): changed_before_var = query.add_variable("changedBefore", "String!") reference_types_var = query.add_variable("referenceTypes", "String!") - project_field = query.add_field_with_edges("project") + project_field = query.add_field("project") project_field.set_filter("name", project_name_var) activities_field = project_field.add_field_with_edges("activities") diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 77222f227..14023265e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1797,7 +1797,7 @@ def get_activities( query.set_variable_value(attr, filter_value) for parsed_data in query.continuous_query(self): - for event in parsed_data["activities"]: + for event in parsed_data["project"]["activities"]: yield event def _endpoint_to_url( From bab024e679e3df93fd93946c0b4230e5a8385fb6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:44:26 +0100 Subject: [PATCH 03/20] added some typehints --- ayon_api/server_api.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 14023265e..b41524da4 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -16,7 +16,8 @@ import warnings import itertools from contextlib import contextmanager -from typing import Optional, Iterable +import typing +from typing import Optional, Iterable, Generator, Dict, List, Any try: from http import HTTPStatus @@ -97,6 +98,19 @@ get_media_mime_type, ) +if typing.TYPE_CHECKING: + from typing import Literal + + ActivityType = Literal[ + "comment", + "watch", + "reviewable", + "status.change", + "assignee.add", + "assignee.remove", + "version.publish" + ] + PatternType = type(re.compile("")) JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) # This should be collected from server schema @@ -1735,7 +1749,7 @@ def get_activities( self, project_name: str, activity_ids: Optional[Iterable[str]] = None, - activity_types: Optional[Iterable[str]] = None, + activity_types: Optional[Iterable["ActivityType"]] = None, entity_ids: Optional[Iterable[str]] = None, entity_names: Optional[Iterable[str]] = None, entity_type: Optional[str] = None, @@ -1743,13 +1757,13 @@ def get_activities( changed_before: Optional[str] = None, reference_types: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, - ): + ) -> Generator[Dict[str, Any], None, None]: """Get activities from server with filtering options. Args: - project_name (str): Project on which event happened. + project_name (str): Project on which activities happened. activity_ids (Optional[Iterable[str]]): Activity ids. - activity_types (Optional[Iterable[str]]): Activity types. + activity_types (Optional[Iterable[ActivityType]]): Activity types. entity_ids (Optional[Iterable[str]]): Entity ids. entity_names (Optional[Iterable[str]]): Entity names. entity_type (Optional[str]): Entity type. @@ -1797,8 +1811,8 @@ def get_activities( query.set_variable_value(attr, filter_value) for parsed_data in query.continuous_query(self): - for event in parsed_data["project"]["activities"]: - yield event + for activity in parsed_data["project"]["activities"]: + yield activity def _endpoint_to_url( self, From aff80627ea971f8224da3386a633ec00f89746f1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:45:05 +0100 Subject: [PATCH 04/20] added method to get single activity --- ayon_api/server_api.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b41524da4..2e2da0ccc 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1814,6 +1814,33 @@ def get_activities( for activity in parsed_data["project"]["activities"]: yield activity + def get_activity_by_id( + self, + project_name: str, + activity_id: str, + fields: Optional[Iterable[str]] = None, + ) -> Optional[Dict[str, Any]]: + """Get activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + + Returns: + Optional[Dict[str, Any]]: Activity data or None if activity is not + found. + + """ + for activity in self.get_activities( + project_name=project_name, + activity_ids={activity_id}, + fields=fields, + ): + return activity + return None + def _endpoint_to_url( self, endpoint: str, From 6e4e978dc6719f906b9c96d613b4b0cc9217eae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 17:45:15 +0100 Subject: [PATCH 05/20] added method to create activity --- ayon_api/server_api.py | 50 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 2e2da0ccc..47f6b8d4d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1841,6 +1841,56 @@ def get_activity_by_id( return activity return None + def create_activity( + self, + project_name: str, + entity_id: str, + entity_type: str, + activity_type: "ActivityType", + activity_id: Optional[str] = None, + body: Optional[str] = None, + file_ids: Optional[List[str]] = None, + timestamp: Optional[str] = None, + data: Optional[Dict[str, Any]] = None, + ): + """Create activity on a project. + + Args: + project_name (str): Project on which activity happened. + entity_id (str): Entity id. + entity_type (str): Entity type. + activity_type (ActivityType): Activity type. + activity_id (Optional[str]): Activity id. + body (Optional[str]): Activity body. + file_ids (Optional[List[str]]): List of file ids attached + to activity. + timestamp (Optional[str]): Activity timestamp. + data (Optional[Dict[str, Any]]): Additional data. + + Returns: + Dict[str, str]: Data with activity id. + + """ + post_data = { + "activityType": activity_type, + } + for key, value in ( + ("id", activity_id), + ("body", body), + ("files", file_ids), + ("timestamp", timestamp), + ("data", data), + ): + if value is not None: + post_data[key] = value + + response = self.post( + f"projects/{project_name}/{entity_type}/{entity_id}/activities", + **post_data + ) + response.raise_for_status() + return response.data + def _endpoint_to_url( self, endpoint: str, From e3497cf89d5710cd2179498dd199c501080d3c5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 18:05:38 +0100 Subject: [PATCH 06/20] added update and delete activity methods --- ayon_api/server_api.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 47f6b8d4d..0b898283f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1891,6 +1891,47 @@ def create_activity( response.raise_for_status() return response.data + def update_activity( + self, + project_name: str, + activity_id: str, + body: str, + file_ids: Optional[List[str]] = None, + ): + """Update activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + body (str): Activity body. + file_ids (Optional[List[str]]): List of file ids attached + to activity. + + """ + data = { + "body": body, + } + if file_ids is not None: + data["files"] = file_ids + response = self.delete( + f"projects/{project_name}/activities/{activity_id}", + **data + ) + response.raise_for_status() + + def delete_activity(self, project_name: str, activity_id: str): + """Delete activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id to remove. + + """ + response = self.delete( + f"projects/{project_name}/activities/{activity_id}" + ) + response.raise_for_status() + def _endpoint_to_url( self, endpoint: str, From 44cb134b71d8772f9652de5a1b8816b70471d435 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 Nov 2024 18:13:50 +0100 Subject: [PATCH 07/20] added activity methods to public api --- ayon_api/__init__.py | 10 +++++ ayon_api/_api.py | 94 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 9eab4ee4a..c3e008886 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -70,6 +70,11 @@ dispatch_event, delete_event, enroll_event_job, + get_activities, + get_activity_by_id, + create_activity, + update_activity, + delete_activity, download_file_to_stream, download_file, upload_file_from_stream, @@ -303,6 +308,11 @@ "dispatch_event", "delete_event", "enroll_event_job", + "get_activities", + "get_activity_by_id", + "create_activity", + "update_activity", + "delete_activity", "download_file_to_stream", "download_file", "upload_file_from_stream", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 898f8b634..63bb0c705 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -877,6 +877,100 @@ def enroll_event_job(*args, **kwargs): return con.enroll_event_job(*args, **kwargs) +def get_activities(*args, **kwargs): + """Get activities from server with filtering options. + + Args: + project_name (str): Project on which activities happened. + activity_ids (Optional[Iterable[str]]): Activity ids. + activity_types (Optional[Iterable[ActivityType]]): Activity types. + entity_ids (Optional[Iterable[str]]): Entity ids. + entity_names (Optional[Iterable[str]]): Entity names. + entity_type (Optional[str]): Entity type. + changed_after (Optional[str]): Return only activities changed + after given iso datetime string. + changed_before (Optional[str]): Return only activities changed + before given iso datetime string. + reference_types (Optional[Iterable[str]]): Reference types. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + + Returns: + Generator[dict[str, Any]]: Available activities matching filters. + + """ + con = get_server_api_connection() + return con.get_activities(*args, **kwargs) + + +def get_activity_by_id(*args, **kwargs): + """Get activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + + Returns: + Optional[Dict[str, Any]]: Activity data or None if activity is not + found. + + """ + con = get_server_api_connection() + return con.get_activity_by_id(*args, **kwargs) + + +def create_activity(*args, **kwargs): + """Create activity on a project. + + Args: + project_name (str): Project on which activity happened. + entity_id (str): Entity id. + entity_type (str): Entity type. + activity_type (ActivityType): Activity type. + activity_id (Optional[str]): Activity id. + body (Optional[str]): Activity body. + file_ids (Optional[List[str]]): List of file ids attached + to activity. + timestamp (Optional[str]): Activity timestamp. + data (Optional[Dict[str, Any]]): Additional data. + + Returns: + Dict[str, str]: Data with activity id. + + """ + con = get_server_api_connection() + return con.create_activity(*args, **kwargs) + + +def update_activity(*args, **kwargs): + """Update activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + body (str): Activity body. + file_ids (Optional[List[str]]): List of file ids attached + to activity. + + """ + con = get_server_api_connection() + return con.update_activity(*args, **kwargs) + + +def delete_activity(*args, **kwargs): + """Delete activity by id. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id to remove. + + """ + con = get_server_api_connection() + return con.delete_activity(*args, **kwargs) + + def download_file_to_stream(*args, **kwargs): """Download file from AYON server to IOStream. From 05427f0a22a22eb397f57aa722036ec6639a54ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:29:05 +0100 Subject: [PATCH 08/20] update activities patch with newer changes --- ayon_api/server_api.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 0b898283f..a5cfb0c71 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1897,6 +1897,8 @@ def update_activity( activity_id: str, body: str, file_ids: Optional[List[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[Dict[str, Any]] = None, ): """Update activity by id. @@ -1906,16 +1908,35 @@ def update_activity( body (str): Activity body. file_ids (Optional[List[str]]): List of file ids attached to activity. + append_file_ids (Optional[bool]): Append file ids to existing + list of file ids. + data (Optional[Dict[str, Any]]): Update data in activity. """ - data = { + update_data = { "body": body, } + major, minor, patch, _, _ = self.server_version_tuple + new_patch_model = (major, minor, patch) > (1, 5, 6) if file_ids is not None: - data["files"] = file_ids + update_data["files"] = file_ids + if new_patch_model: + update_data["appendFiles"] = append_file_ids + elif append_file_ids: + raise ValueError( + "Append file ids is supported after server version 1.5.6." + ) + + if data is not None: + if not new_patch_model: + raise ValueError( + "Update of data is supported after server version 1.5.6." + ) + update_data["data"] = data + response = self.delete( f"projects/{project_name}/activities/{activity_id}", - **data + **update_data ) response.raise_for_status() From a53441676dd256093a456eb98190840d737629af Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:30:47 +0100 Subject: [PATCH 09/20] fix graphql filter types --- ayon_api/graphql_queries.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index e44cd07bc..765ed96a4 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -641,10 +641,10 @@ def users_graphql_query(fields): def activities_graphql_query(fields): query = GraphQlQuery("Activities") project_name_var = query.add_variable("projectName", "String!") - activity_ids_var = query.add_variable("activityIds", "[String]") - activity_types_var = query.add_variable("activityTypes", "[String]") - entity_ids_var = query.add_variable("entityIds", "[String]") - entity_names_var = query.add_variable("entityNames", "[String]") + activity_ids_var = query.add_variable("activityIds", "[String!]") + activity_types_var = query.add_variable("activityTypes", "[String!]") + entity_ids_var = query.add_variable("entityIds", "[String!]") + entity_names_var = query.add_variable("entityNames", "[String!]") entity_type_var = query.add_variable("entityType", "String!") changed_after_var = query.add_variable("changedAfter", "String!") changed_before_var = query.add_variable("changedBefore", "String!") From 5f22a12d71fc61cf25ce019a467291d973f8173b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:11:17 +0100 Subject: [PATCH 10/20] allow to not update 'body' --- ayon_api/server_api.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a5cfb0c71..927925117 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -110,6 +110,13 @@ "assignee.remove", "version.publish" ] + ActivityReferenceType = Literal[ + "origin", + "mention", + "author", + "relation", + "watching", + ] PatternType = type(re.compile("")) JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) @@ -1755,7 +1762,7 @@ def get_activities( entity_type: Optional[str] = None, changed_after: Optional[str] = None, changed_before: Optional[str] = None, - reference_types: Optional[Iterable[str]] = None, + reference_types: Optional[Iterable["ActivityReferenceType"]] = None, fields: Optional[Iterable[str]] = None, ) -> Generator[Dict[str, Any], None, None]: """Get activities from server with filtering options. @@ -1771,7 +1778,8 @@ def get_activities( after given iso datetime string. changed_before (Optional[str]): Return only activities changed before given iso datetime string. - reference_types (Optional[Iterable[str]]): Reference types. + reference_types (Optional[Iterable[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. fields (Optional[Iterable[str]]): Fields that should be received for each activity. @@ -1784,6 +1792,8 @@ def get_activities( filters = { "projectName": project_name, } + if reference_types is None: + reference_types = {"origin"} if not _prepare_list_filters( filters, @@ -1818,6 +1828,7 @@ def get_activity_by_id( self, project_name: str, activity_id: str, + reference_types: Optional[Iterable["ActivityReferenceType"]] = None, fields: Optional[Iterable[str]] = None, ) -> Optional[Dict[str, Any]]: """Get activity by id. @@ -1836,6 +1847,7 @@ def get_activity_by_id( for activity in self.get_activities( project_name=project_name, activity_ids={activity_id}, + reference_types=reference_types, fields=fields, ): return activity @@ -1895,7 +1907,7 @@ def update_activity( self, project_name: str, activity_id: str, - body: str, + body: Optional[str] = None, file_ids: Optional[List[str]] = None, append_file_ids: Optional[bool] = False, data: Optional[Dict[str, Any]] = None, @@ -1913,11 +1925,18 @@ def update_activity( data (Optional[Dict[str, Any]]): Update data in activity. """ - update_data = { - "body": body, - } + update_data = {} major, minor, patch, _, _ = self.server_version_tuple new_patch_model = (major, minor, patch) > (1, 5, 6) + if body is None and not new_patch_model: + raise ValueError( + "Update without 'body' is supported" + " after server version 1.5.6." + ) + + if body is not None: + update_data["body"] = body + if file_ids is not None: update_data["files"] = file_ids if new_patch_model: From 63a771bf1ec7d39b80d2b53662658216e2b14343 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:37:13 +0100 Subject: [PATCH 11/20] fix type of reference types --- ayon_api/graphql_queries.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 765ed96a4..999c9e5c2 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -648,7 +648,7 @@ def activities_graphql_query(fields): entity_type_var = query.add_variable("entityType", "String!") changed_after_var = query.add_variable("changedAfter", "String!") changed_before_var = query.add_variable("changedBefore", "String!") - reference_types_var = query.add_variable("referenceTypes", "String!") + reference_types_var = query.add_variable("referenceTypes", "[String!]") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) From b1cde94f1546234b8393ac424097855ced7f9594 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:42:36 +0100 Subject: [PATCH 12/20] use patch instead of delete --- ayon_api/server_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 927925117..bc240b38e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1953,7 +1953,7 @@ def update_activity( ) update_data["data"] = data - response = self.delete( + response = self.patch( f"projects/{project_name}/activities/{activity_id}", **update_data ) From 31e48d38c2e8f25263002a506b885b49948a3263 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:09:52 +0100 Subject: [PATCH 13/20] convert 'activityData' to dictionary --- ayon_api/server_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index bc240b38e..c25bf7850 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1822,6 +1822,9 @@ def get_activities( for parsed_data in query.continuous_query(self): for activity in parsed_data["project"]["activities"]: + activity_data = activity.get("activityData") + if isinstance(activity_data, str): + activity["activityData"] = json.loads(activity_data) yield activity def get_activity_by_id( From 449f0ea701296558c94eee3b9c9c767b5d5c26ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:36:24 +0100 Subject: [PATCH 14/20] implemented batch send of activities operations --- ayon_api/server_api.py | 55 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index c25bf7850..4ac5198be 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -8544,6 +8544,59 @@ def send_batch_operations( list[dict[str, Any]]: Operations result with process details. """ + return self._send_batch_operations( + f"projects/{project_name}/operations", + operations, + can_fail, + raise_on_fail, + ) + + def send_activities_batch_operations( + self, + project_name, + operations, + can_fail=False, + raise_on_fail=True + ): + """Post multiple CRUD activities operations to server. + + When multiple changes should be made on server side this is the best + way to go. It is possible to pass multiple operations to process on a + server side and do the changes in a transaction. + + Args: + project_name (str): On which project should be operations + processed. + operations (list[dict[str, Any]]): Operations to be processed. + can_fail (Optional[bool]): Server will try to process all + operations even if one of them fails. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. + + Raises: + ValueError: Operations can't be converted to json string. + FailedOperations: When output does not contain server operations + or 'raise_on_fail' is enabled and any operation fails. + + Returns: + list[dict[str, Any]]: Operations result with process details. + + """ + return self._send_batch_operations( + f"projects/{project_name}/operations/activities", + operations, + can_fail, + raise_on_fail, + ) + + def _send_batch_operations( + self, + uri: str, + operations: List[Dict[str, Any]], + can_fail: bool, + raise_on_fail: bool + ): if not operations: return [] @@ -8576,7 +8629,7 @@ def send_batch_operations( return [] result = self.post( - "projects/{}/operations".format(project_name), + uri, operations=operations_body, canFail=can_fail ) From 6d9dc4f8713e1e5b6cb65af94058353543416c6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Nov 2024 17:36:55 +0100 Subject: [PATCH 15/20] updated public api --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 36 +++++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index c3e008886..618201cbf 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -235,6 +235,7 @@ get_representations_links, get_representation_links, send_batch_operations, + send_activities_batch_operations, ) @@ -473,4 +474,5 @@ "get_representations_links", "get_representation_links", "send_batch_operations", + "send_activities_batch_operations", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 63bb0c705..67ee7ff04 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -891,7 +891,8 @@ def get_activities(*args, **kwargs): after given iso datetime string. changed_before (Optional[str]): Return only activities changed before given iso datetime string. - reference_types (Optional[Iterable[str]]): Reference types. + reference_types (Optional[Iterable[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. fields (Optional[Iterable[str]]): Fields that should be received for each activity. @@ -953,6 +954,9 @@ def update_activity(*args, **kwargs): body (str): Activity body. file_ids (Optional[List[str]]): List of file ids attached to activity. + append_file_ids (Optional[bool]): Append file ids to existing + list of file ids. + data (Optional[Dict[str, Any]]): Update data in activity. """ con = get_server_api_connection() @@ -4509,3 +4513,33 @@ def send_batch_operations(*args, **kwargs): """ con = get_server_api_connection() return con.send_batch_operations(*args, **kwargs) + + +def send_activities_batch_operations(*args, **kwargs): + """Post multiple CRUD activities operations to server. + + When multiple changes should be made on server side this is the best + way to go. It is possible to pass multiple operations to process on a + server side and do the changes in a transaction. + + Args: + project_name (str): On which project should be operations + processed. + operations (list[dict[str, Any]]): Operations to be processed. + can_fail (Optional[bool]): Server will try to process all + operations even if one of them fails. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. + + Raises: + ValueError: Operations can't be converted to json string. + FailedOperations: When output does not contain server operations + or 'raise_on_fail' is enabled and any operation fails. + + Returns: + list[dict[str, Any]]: Operations result with process details. + + """ + con = get_server_api_connection() + return con.send_activities_batch_operations(*args, **kwargs) From db3398c53872f9136f65ad3d626f95aeb4e8ee88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:01:28 +0100 Subject: [PATCH 16/20] moved types to separate file --- ayon_api/_typing.py | 19 +++++++++++++++++++ ayon_api/server_api.py | 19 +------------------ 2 files changed, 20 insertions(+), 18 deletions(-) create mode 100644 ayon_api/_typing.py diff --git a/ayon_api/_typing.py b/ayon_api/_typing.py new file mode 100644 index 000000000..566f598ab --- /dev/null +++ b/ayon_api/_typing.py @@ -0,0 +1,19 @@ +from typing import Literal + +ActivityType = Literal[ + "comment", + "watch", + "reviewable", + "status.change", + "assignee.add", + "assignee.remove", + "version.publish" +] + +ActivityReferenceType = Literal[ + "origin", + "mention", + "author", + "relation", + "watching", +] diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d8a0317a4..f320a0f43 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -99,24 +99,7 @@ ) if typing.TYPE_CHECKING: - from typing import Literal - - ActivityType = Literal[ - "comment", - "watch", - "reviewable", - "status.change", - "assignee.add", - "assignee.remove", - "version.publish" - ] - ActivityReferenceType = Literal[ - "origin", - "mention", - "author", - "relation", - "watching", - ] + from ._typing import ActivityType, ActivityReferenceType PatternType = type(re.compile("")) JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) From 23c6d5e11b55690f8fe17ddb991ca90d9ec59327 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:02:16 +0100 Subject: [PATCH 17/20] enhanced automated api to work correctly for return type --- automated_api.py | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/automated_api.py b/automated_api.py index ee5e661d6..9b77af287 100644 --- a/automated_api.py +++ b/automated_api.py @@ -114,25 +114,40 @@ def prepare_docstring(func): return f'"""{docstring}{line_char}\n"""' -def _get_typehint(param, api_globals): - if param.annotation is inspect.Parameter.empty: - return None - - an = param.annotation - if inspect.isclass(an): - return an.__name__ +def _get_typehint(annotation, api_globals): + if inspect.isclass(annotation): + return annotation.__name__ + + typehint = ( + str(annotation) + .replace("typing.", "") + .replace("NoneType", "None") + ) + forwardref_regex = re.compile( + "(?PForwardRef\('(?P[a-zA-Z0-9]+)'\))" + ) + for item in forwardref_regex.finditer(str(typehint)): + groups = item.groupdict() + name = groups["name"] + typehint = typehint.replace(groups["full"], f'"{name}"') - typehint = str(an).replace("typing.", "") try: # Test if typehint is valid for known '_api' content exec(f"_: {typehint} = None", api_globals) except NameError: + print("Unknown typehint:", typehint) typehint = f'"{typehint}"' return typehint +def _get_param_typehint(param, api_globals): + if param.annotation is inspect.Parameter.empty: + return None + return _get_typehint(param.annotation, api_globals) + + def _add_typehint(param_name, param, api_globals): - typehint = _get_typehint(param, api_globals) + typehint = _get_param_typehint(param, api_globals) if not typehint: return param_name return f"{param_name}: {typehint}" @@ -154,7 +169,7 @@ def _kw_default_to_str(param_name, param, api_globals): raise TypeError("Unknown default value type") else: default = repr(default) - typehint = _get_typehint(param, api_globals) + typehint = _get_param_typehint(param, api_globals) if typehint: return f"{param_name}: {typehint} = {default}" return f"{param_name}={default}" @@ -216,7 +231,8 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): func_params_str = f"(\n{lines_str}\n{base_indent_str})" if sig.return_annotation is not inspect.Signature.empty: - func_params_str += f" -> {sig.return_annotation}" + return_typehint = _get_typehint(sig.return_annotation, api_globals) + func_params_str += f" -> {return_typehint}" body_params_str = "()" if body_params: From 9a9f3d63558c389d7433409a0fd31bc1bdcb52ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:02:28 +0100 Subject: [PATCH 18/20] apply changes in _api.py --- ayon_api/_api.py | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index aa97825f1..ec7b36a18 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -11,7 +11,8 @@ import os import socket -from typing import Optional +import typing +from typing import Optional, List, Dict, Iterable, Generator, Any from .constants import ( SERVER_URL_ENV_KEY, @@ -24,6 +25,9 @@ get_default_settings_variant as _get_default_settings_variant, ) +if typing.TYPE_CHECKING: + from ._typing import ActivityType, ActivityReferenceType + class GlobalServerAPI(ServerAPI): """Extended server api which also handles storing tokens and url. @@ -1123,16 +1127,16 @@ def enroll_event_job( def get_activities( project_name: str, - activity_ids: "Optional[Iterable[str]]" = None, - activity_types: "Optional[Iterable[ForwardRef('ActivityType')]]" = None, - entity_ids: "Optional[Iterable[str]]" = None, - entity_names: "Optional[Iterable[str]]" = None, + activity_ids: Optional[Iterable[str]] = None, + activity_types: Optional[Iterable["ActivityType"]] = None, + entity_ids: Optional[Iterable[str]] = None, + entity_names: Optional[Iterable[str]] = None, entity_type: Optional[str] = None, changed_after: Optional[str] = None, changed_before: Optional[str] = None, - reference_types: "Optional[Iterable[ForwardRef('ActivityReferenceType')]]" = None, - fields: "Optional[Iterable[str]]" = None, -) -> typing.Generator[typing.Dict[str, typing.Any], NoneType, NoneType]: + reference_types: Optional[Iterable["ActivityReferenceType"]] = None, + fields: Optional[Iterable[str]] = None, +) -> Generator[Dict[str, Any], None, None]: """Get activities from server with filtering options. Args: @@ -1173,9 +1177,9 @@ def get_activities( def get_activity_by_id( project_name: str, activity_id: str, - reference_types: "Optional[Iterable[ForwardRef('ActivityReferenceType')]]" = None, - fields: "Optional[Iterable[str]]" = None, -) -> typing.Optional[typing.Dict[str, typing.Any]]: + reference_types: Optional[Iterable["ActivityReferenceType"]] = None, + fields: Optional[Iterable[str]] = None, +) -> Optional[Dict[str, Any]]: """Get activity by id. Args: @@ -1205,9 +1209,9 @@ def create_activity( activity_type: "ActivityType", activity_id: Optional[str] = None, body: Optional[str] = None, - file_ids: "Optional[List[str]]" = None, + file_ids: Optional[List[str]] = None, timestamp: Optional[str] = None, - data: "Optional[Dict[str, Any]]" = None, + data: Optional[Dict[str, Any]] = None, ): """Create activity on a project. @@ -1245,9 +1249,9 @@ def update_activity( project_name: str, activity_id: str, body: Optional[str] = None, - file_ids: "Optional[List[str]]" = None, + file_ids: Optional[List[str]] = None, append_file_ids: Optional[bool] = False, - data: "Optional[Dict[str, Any]]" = None, + data: Optional[Dict[str, Any]] = None, ): """Update activity by id. From 3d7fcaaea8a5613f08e1146a82042fcdaaae6986 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 17:04:55 +0100 Subject: [PATCH 19/20] change return from create activity --- ayon_api/_api.py | 4 ++-- ayon_api/server_api.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index ec7b36a18..b1af5f6cd 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1212,7 +1212,7 @@ def create_activity( file_ids: Optional[List[str]] = None, timestamp: Optional[str] = None, data: Optional[Dict[str, Any]] = None, -): +) -> str: """Create activity on a project. Args: @@ -1228,7 +1228,7 @@ def create_activity( data (Optional[Dict[str, Any]]): Additional data. Returns: - Dict[str, str]: Data with activity id. + str: Activity id. """ con = get_server_api_connection() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f320a0f43..e4e60c58b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1901,7 +1901,7 @@ def create_activity( file_ids: Optional[List[str]] = None, timestamp: Optional[str] = None, data: Optional[Dict[str, Any]] = None, - ): + ) -> str: """Create activity on a project. Args: @@ -1917,7 +1917,7 @@ def create_activity( data (Optional[Dict[str, Any]]): Additional data. Returns: - Dict[str, str]: Data with activity id. + str: Activity id. """ post_data = { @@ -1938,7 +1938,7 @@ def create_activity( **post_data ) response.raise_for_status() - return response.data + return response.data["id"] def update_activity( self, From a9615fc87fca3fc4b477c9f2a4ec598435aa6309 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 22 Nov 2024 19:10:54 +0100 Subject: [PATCH 20/20] excape regex with 'r' --- automated_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automated_api.py b/automated_api.py index 9b77af287..a764663c2 100644 --- a/automated_api.py +++ b/automated_api.py @@ -124,7 +124,7 @@ def _get_typehint(annotation, api_globals): .replace("NoneType", "None") ) forwardref_regex = re.compile( - "(?PForwardRef\('(?P[a-zA-Z0-9]+)'\))" + r"(?PForwardRef\('(?P[a-zA-Z0-9]+)'\))" ) for item in forwardref_regex.finditer(str(typehint)): groups = item.groupdict()