From ec847d8d371366d4c7354a933475f7b12bd2bfcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 16 Dec 2024 16:56:28 +0100 Subject: [PATCH 001/506] :art: add support for representation traits --- ayon_api/_api.py | 7 +++++++ ayon_api/server_api.py | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 3b9fc9823..1fc623d20 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5476,6 +5476,7 @@ def create_representation( status: Optional[str] = None, active: Optional[bool] = None, representation_id: Optional[str] = None, + traits: Optional[Dict[str, Any]] = None, ) -> str: """Create new representation. @@ -5491,6 +5492,8 @@ def create_representation( active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not passed new id is generated. + traits (Optional[dict[str, Any]]): Representation traits + serialized as dict. Returns: str: Representation id. @@ -5508,6 +5511,7 @@ def create_representation( status=status, active=active, representation_id=representation_id, + traits=traits, ) @@ -5522,6 +5526,7 @@ def update_representation( tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, + traits: Optional[Dict[str, Any]] = None, ): """Update representation entity on server. @@ -5542,6 +5547,7 @@ def update_representation( tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. + traits (Optional[dict[str, Any]]): New traits. """ con = get_server_api_connection() @@ -5556,6 +5562,7 @@ def update_representation( tags=tags, status=status, active=active, + traits=traits, ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b6ede5e73..685579e4a 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7498,6 +7498,7 @@ def create_representation( status: Optional[str] = None, active: Optional[bool] = None, representation_id: Optional[str] = None, + traits: Optional[Dict[str, Any]] = None, ) -> str: """Create new representation. @@ -7513,6 +7514,8 @@ def create_representation( active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not passed new id is generated. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. Returns: str: Representation id. @@ -7532,6 +7535,7 @@ def create_representation( ("tags", tags), ("status", status), ("active", active), + ("traits", traits), ): if value is not None: create_data[key] = value @@ -7555,6 +7559,7 @@ def update_representation( tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, + traits: Optional[Dict[str, Any]] = None, ): """Update representation entity on server. @@ -7587,6 +7592,7 @@ def update_representation( ("tags", tags), ("status", status), ("active", active), + ("traits", traits), ): if value is not None: update_data[key] = value From 265688a868ece4cd908cf8d238d4a976cfa4ecba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 6 Mar 2025 11:05:40 +0100 Subject: [PATCH 002/506] add missing taskIds filtering to versions graphql query --- ayon_api/graphql_queries.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 6ef2ca341..e6a58ce58 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -341,6 +341,7 @@ def versions_graphql_query(fields): project_name_var = query.add_variable("projectName", "String!") product_ids_var = query.add_variable("productIds", "[String!]") version_ids_var = query.add_variable("versionIds", "[String!]") + task_ids_var = query.add_variable("taskIds", "[String!]") versions_var = query.add_variable("versions", "[Int!]") hero_only_var = query.add_variable("heroOnly", "Boolean") latest_only_var = query.add_variable("latestOnly", "Boolean") @@ -357,6 +358,7 @@ def versions_graphql_query(fields): versions_field.set_filter("ids", version_ids_var) versions_field.set_filter("productIds", product_ids_var) versions_field.set_filter("versions", versions_var) + versions_field.set_filter("taskIds", task_ids_var) versions_field.set_filter("heroOnly", hero_only_var) versions_field.set_filter("latestOnly", latest_only_var) versions_field.set_filter("heroOrLatestOnly", hero_or_latest_only_var) From 1d402b4a60742d4ed43af9f09c4fe2c2ba56fff1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 17 Mar 2025 13:59:22 +0100 Subject: [PATCH 003/506] added emails filtering --- ayon_api/graphql_queries.py | 2 ++ ayon_api/server_api.py | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index e6a58ce58..18b76f059 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -619,10 +619,12 @@ def events_graphql_query(fields, order, use_states=False): def users_graphql_query(fields): query = GraphQlQuery("Users") names_var = query.add_variable("userNames", "[String!]") + emails_var = query.add_variable("emails", "[String!]") project_name_var = query.add_variable("projectName", "String!") users_field = query.add_field_with_edges("users") users_field.set_filter("names", names_var) + users_field.set_filter("emails", emails_var) users_field.set_filter("projectName", project_name_var) nested_fields = fields_to_dict(set(fields)) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f1b7ff49c..c89f7b750 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1143,6 +1143,7 @@ def get_users( self, project_name: Optional[str] = None, usernames: Optional[Iterable[str]] = None, + emails: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, ) -> Generator[Dict[str, Any], None, None]: """Get Users. @@ -1153,6 +1154,7 @@ def get_users( Args: project_name (Optional[str]): Project name. usernames (Optional[Iterable[str]]): Filter by usernames. + emails (Optional[Iterable[str]]): Filter by emails. fields (Optional[Iterable[str]]): Fields to be queried for users. @@ -1167,6 +1169,22 @@ def get_users( return filters["userNames"] = list(usernames) + if emails is not None: + emails = set(emails) + if not emails: + return + + major, minor, patch, _, _ = self.server_version_tuple + emails_filter_available = (major, minor, patch) > (1, 7, 3) + if not emails_filter_available: + server_version = self.get_server_version() + raise ValueError( + "Filtering by emails is not supported by" + f" server version {server_version}." + ) + + filters["emails"] = list(emails) + if project_name is not None: filters["projectName"] = project_name From f7c397cc8eb18cb3fe03d271a0f98f4ae8684f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Fri, 28 Mar 2025 18:39:21 +0100 Subject: [PATCH 004/506] =?UTF-8?q?=E2=9C=A8=20add=20traits=20to=20represe?= =?UTF-8?q?ntation=20fields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit add traits as the default field for the representation --- ayon_api/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 4d5700ce9..93ff2877f 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -131,6 +131,7 @@ "data", "status", "tags", + "traits", } REPRESENTATION_FILES_FIELDS = { From d0f560df79f4649be0b4db6d2c056bee0f436a74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 7 Apr 2025 14:56:36 +0200 Subject: [PATCH 005/506] :recycle: remove traits from default fields this is to maintain compatibility with older server versions --- ayon_api/constants.py | 1 - ayon_api/server_api.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 93ff2877f..4d5700ce9 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -131,7 +131,6 @@ "data", "status", "tags", - "traits", } REPRESENTATION_FILES_FIELDS = { diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 93edfc246..be1be2518 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -521,6 +521,7 @@ def __init__( self._server_version_tuple = None self._graphql_allows_data_in_query = None + self._grahql_allows_traits_in_representations = None self._session = None @@ -1116,6 +1117,20 @@ def graphql_allows_data_in_query(self) -> bool: self._graphql_allows_data_in_query = graphql_allows_data_in_query return self._graphql_allows_data_in_query + + @property + def grahql_allows_traits_in_representations(self) -> bool: + """Check server support for representation traits.""" + if self._grahql_allows_traits_in_representations is None: + major, minor, patch, _, _ = self.server_version_tuple + grahql_allows_traits_in_representations = True + if (major, minor, patch) < (1, 7, 5): + grahql_allows_traits_in_representations = False + self._grahql_allows_traits_in_representations = \ + grahql_allows_traits_in_representations + return self._grahql_allows_traits_in_representations + + def _get_user_info(self) -> Optional[Dict[str, Any]]: if self._access_token is None: return None @@ -2761,6 +2776,9 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: if not self.graphql_allows_data_in_query: entity_type_defaults.discard("data") + if not self.grahql_allows_traits_in_representations: + entity_type_defaults.discard("traits") + elif entity_type == "folderType": entity_type_defaults = set(DEFAULT_FOLDER_TYPE_FIELDS) From 96b19f697cfac022a35cec25b159796dbd54d3f5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 15:19:36 +0200 Subject: [PATCH 006/506] pass in entity id --- ayon_api/operations.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 04fe8714d..b6805b379 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1188,7 +1188,10 @@ def update_product( update_data[key] = value return self.update_entity( - project_name, "product", update_data + project_name, + "product", + product_id, + update_data ) def delete_product(self, project_name, product_id): @@ -1456,7 +1459,10 @@ def update_representation( update_data[key] = value return self.update_entity( - project_name, "representation", update_data + project_name, + "representation", + representation_id, + update_data ) def delete_representation(self, project_name, representation_id): From 6308cfed992e27baf186f57efcdae9c5df9c1b71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:19:43 +0200 Subject: [PATCH 007/506] added task to allowed thumbnail entity types --- ayon_api/server_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f1b7ff49c..5b3631757 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7830,6 +7830,7 @@ def get_thumbnail( if entity_type in ( "folder", + "task", "version", "workfile", ): From b685d7ab27b7b840756b30490f4fd0e98e2c0c29 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:28:12 +0200 Subject: [PATCH 008/506] modified docstrings --- ayon_api/server_api.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 5b3631757..fa7ef322b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7765,9 +7765,10 @@ def get_thumbnail_by_id( ) -> ThumbnailContent: """Get thumbnail from server by id. - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity type is required - to be passed. + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. Notes: It is recommended to use one of prepared entity type specific @@ -7802,7 +7803,7 @@ def get_thumbnail( """Get thumbnail from server. Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity type is required + be queried per entity. So an entity type and entity id is required to be passed. Notes: From ba885c980acfd9fcd4835105f1c3aba96ed4085d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:28:38 +0200 Subject: [PATCH 009/506] added deprecation warnings for thumbnail id --- ayon_api/server_api.py | 38 ++++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index fa7ef322b..167c4149b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7827,7 +7827,13 @@ def get_thumbnail( """ if thumbnail_id: - return self.get_thumbnail_by_id(project_name, thumbnail_id) + warnings.warn( + ( + "Function 'get_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) if entity_type in ( "folder", @@ -7861,8 +7867,16 @@ def get_folder_thumbnail( valid. """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_folder_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) return self.get_thumbnail( - project_name, "folder", folder_id, thumbnail_id + project_name, "folder", folder_id ) def get_version_thumbnail( @@ -7885,8 +7899,16 @@ def get_version_thumbnail( valid. """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_version_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) return self.get_thumbnail( - project_name, "version", version_id, thumbnail_id + project_name, "version", version_id ) def get_workfile_thumbnail( @@ -7909,8 +7931,16 @@ def get_workfile_thumbnail( valid. """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_workfile_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) return self.get_thumbnail( - project_name, "workfile", workfile_id, thumbnail_id + project_name, "workfile", workfile_id ) def create_thumbnail( From 6f3d4a1bcd8ce8eb8003770f1957736701561dde Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:28:46 +0200 Subject: [PATCH 010/506] added dedicated method for task --- ayon_api/server_api.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 167c4149b..96ebf55a7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7879,6 +7879,24 @@ def get_folder_thumbnail( project_name, "folder", folder_id ) + def get_task_thumbnail( + self, + project_name: str, + task_id: str, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + return self.get_thumbnail(project_name, "folder", folder_id) + def get_version_thumbnail( self, project_name: str, From 0440bf28ceae8763657a0e9e7538bf152752e9f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:29:33 +0200 Subject: [PATCH 011/506] fix variable name --- 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 96ebf55a7..8715bb600 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7895,7 +7895,7 @@ def get_task_thumbnail( valid. """ - return self.get_thumbnail(project_name, "folder", folder_id) + return self.get_thumbnail(project_name, "folder", task_id) def get_version_thumbnail( self, From 7bf3707d3d037a26856a75a55599ce66bbadfb85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:33:12 +0200 Subject: [PATCH 012/506] update public api --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 31 +++++++++++++++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index fdec19cce..373cbad88 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -214,6 +214,7 @@ get_thumbnail_by_id, get_thumbnail, get_folder_thumbnail, + get_task_thumbnail, get_version_thumbnail, get_workfile_thumbnail, create_thumbnail, @@ -459,6 +460,7 @@ "get_thumbnail_by_id", "get_thumbnail", "get_folder_thumbnail", + "get_task_thumbnail", "get_version_thumbnail", "get_workfile_thumbnail", "create_thumbnail", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 3b9fc9823..9dbbc05b9 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5698,9 +5698,10 @@ def get_thumbnail_by_id( ) -> ThumbnailContent: """Get thumbnail from server by id. - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity type is required - to be passed. + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. Notes: It is recommended to use one of prepared entity type specific @@ -5736,7 +5737,7 @@ def get_thumbnail( """Get thumbnail from server. Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity type is required + be queried per entity. So an entity type and entity id is required to be passed. Notes: @@ -5794,6 +5795,28 @@ def get_folder_thumbnail( ) +def get_task_thumbnail( + project_name: str, + task_id: str, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_task_thumbnail( + project_name=project_name, + task_id=task_id, + ) + + def get_version_thumbnail( project_name: str, version_id: str, From 32e5612f3a87503835225cf48fd4ccfea95f0a82 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:40:34 +0200 Subject: [PATCH 013/506] fix active filtering --- ayon_api/server_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 5b3631757..87f939e45 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4646,10 +4646,14 @@ def get_projects( return self._prepare_fields("project", fields, own_attributes) + if active is not None: + fields.add("active") query = projects_graphql_query(fields) for parsed_data in query.continuous_query(self): for project in parsed_data["projects"]: + if active is not None and active is not project["active"]: + continue if own_attributes: fill_own_attribs(project) yield project From f16b5839a4ad151735e1fafc559a1e0b4b53b4d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 7 Apr 2025 16:48:51 +0200 Subject: [PATCH 014/506] fix line length --- ayon_api/server_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 8715bb600..9c4fece17 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7952,8 +7952,9 @@ def get_workfile_thumbnail( if thumbnail_id: warnings.warn( ( - "Function 'get_workfile_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." + "Function 'get_workfile_thumbnail' got 'thumbnail_id'" + " which is deprecated and will be removed in future" + " version." ), DeprecationWarning ) From a5640041e229870ada6c8b368c08cbabbda1703a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:49:53 +0200 Subject: [PATCH 015/506] removed appdirs from dependencies --- poetry.lock | 14 +------------- pyproject.toml | 2 -- 2 files changed, 1 insertion(+), 15 deletions(-) diff --git a/poetry.lock b/poetry.lock index f2c892ecc..4cde77290 100644 --- a/poetry.lock +++ b/poetry.lock @@ -12,18 +12,6 @@ files = [ {file = "alabaster-0.7.13.tar.gz", hash = "sha256:a27a4a084d5e690e16e01e03ad2b2e552c61a65469419b907243193de1a84ae2"}, ] -[[package]] -name = "appdirs" -version = "1.4.4" -description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, - {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, -] - [[package]] name = "astroid" version = "2.11.7" @@ -967,4 +955,4 @@ testing = ["func-timeout", "jaraco.itertools", "pytest (>=4.6)", "pytest-black ( [metadata] lock-version = "2.1" python-versions = ">=3.6.5" -content-hash = "5000b7799750aee6806e100abe2246c1d207b9d4444a1c47ad76d343598e9099" +content-hash = "0d5fd126843b2155bfd8c4d501d3dd62c874a05457ead367debb1534712e5f40" diff --git a/pyproject.toml b/pyproject.toml index 05ad58922..b2de6967d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ classifiers = [ dependencies = [ "requests >= 2.27.1", "Unidecode >= 1.3.0", - "appdirs >=1, <2", ] [project.urls] @@ -42,7 +41,6 @@ packages = [ python = ">=3.6.5" requests = "^2.27" Unidecode = "^1.3" -appdirs = "^1.4" [tool.poetry.group.dev.dependencies] sphinx = "*" From f6fa879134214d5ee8a0617608770a903f363497 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 8 Apr 2025 10:50:26 +0200 Subject: [PATCH 016/506] update public api --- ayon_api/_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 3b9fc9823..4d1e7c8e5 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -706,6 +706,7 @@ def get_server_version_tuple() -> Tuple[int, int, int, str, str]: def get_users( project_name: Optional[str] = None, usernames: Optional[Iterable[str]] = None, + emails: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, ) -> Generator[Dict[str, Any], None, None]: """Get Users. @@ -716,6 +717,7 @@ def get_users( Args: project_name (Optional[str]): Project name. usernames (Optional[Iterable[str]]): Filter by usernames. + emails (Optional[Iterable[str]]): Filter by emails. fields (Optional[Iterable[str]]): Fields to be queried for users. @@ -727,6 +729,7 @@ def get_users( return con.get_users( project_name=project_name, usernames=usernames, + emails=emails, fields=fields, ) From 71df6831173b4f5e3260b5599972339a6702646e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 9 Apr 2025 11:10:03 +0200 Subject: [PATCH 017/506] :sparkles: add traits to operations --- ayon_api/operations.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 04fe8714d..701cf86e8 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -276,7 +276,8 @@ def new_representation_entity( tags=None, attribs=None, data=None, - entity_id=None + entity_id=None, + traits=None, ): """Create skeleton data of representation entity. @@ -292,6 +293,8 @@ def new_representation_entity( data (Optional[Dict[str, Any]]): Representation entity data. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. + traits (Optional[Dict[str, Any]]): Representation traits. Empty + if not passed. Returns: Dict[str, Any]: Skeleton of representation entity. @@ -309,7 +312,8 @@ def new_representation_entity( "files": files, "name": name, "data": data, - "attrib": attribs + "attrib": attribs, + "traits": traits or {}, } if tags: output["tags"] = tags @@ -1360,6 +1364,7 @@ def create_representation( status=None, active=None, representation_id=None, + traits=None, ): """Create new representation. @@ -1375,6 +1380,8 @@ def create_representation( active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not passed new id is generated. + traits (Optional[Dict[str, Any]]): Representation traits. Empty + if not passed. Returns: CreateOperation: Object of create operation. @@ -1394,6 +1401,7 @@ def create_representation( ("tags", tags), ("status", status), ("active", active), + ("traits", traits), ): if value is not None: create_data[key] = value @@ -1416,6 +1424,7 @@ def update_representation( tags=None, status=None, active=None, + traits=None, ): """Update representation entity on server. @@ -1436,6 +1445,7 @@ def update_representation( tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. + traits (Optional[Dict[str, Any]]): New representation traits. Returns: UpdateOperation: Object of update operation. @@ -1451,6 +1461,7 @@ def update_representation( ("tags", tags), ("status", status), ("active", active), + ("traits", traits), ): if value is not None: update_data[key] = value From 891f637fc3b42582f7feca3f8ef9a95849bd693c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 9 Apr 2025 11:10:31 +0200 Subject: [PATCH 018/506] :bug: fix type error --- 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 be1be2518..5c5d558e9 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -521,7 +521,7 @@ def __init__( self._server_version_tuple = None self._graphql_allows_data_in_query = None - self._grahql_allows_traits_in_representations = None + self._grahql_allows_traits_in_representations: Optional[bool] = None self._session = None From 93a8c376525366e94e52705f06ccd722d0956f8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 9 Apr 2025 11:10:45 +0200 Subject: [PATCH 019/506] :memo: add todo for future --- ayon_api/constants.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 4d5700ce9..9fd0f990a 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -131,6 +131,8 @@ "data", "status", "tags", + # TODO (antirotor): traits should be there in time when server + # usage prior to 1.7.5 is not supported (used) anymore. } REPRESENTATION_FILES_FIELDS = { From 10aad31b32a7dd3f6f54822f66c72ab5cde75f6a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:07:39 +0200 Subject: [PATCH 020/506] fix jpeg detection --- ayon_api/utils.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index bd391b3cb..11d5c8d99 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -855,11 +855,11 @@ def _get_media_mime_type_for_content_base(content: bytes) -> Optional[str]: if content[0:4] == b"\211PNG": return "image/png" - # JPEG, JFIF or Exif - if ( - content[0:4] == b"\xff\xd8\xff\xdb" - or content[6:10] in (b"JFIF", b"Exif") - ): + # JPEG + # - [0:2] is constant b"\xff\xd8" + # - [2:4] Marker identifier b"\xff{?}" + # NOTE: File ends with b"\xff\xd9" + if content[0:3] == b"\xff\xd8\xff": return "image/jpeg" # Webp From 73c2d8d38a61b1daf4d06b6bd9ed90976d7b2c98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Apr 2025 15:08:24 +0200 Subject: [PATCH 021/506] added references --- ayon_api/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 11d5c8d99..5529f121f 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -857,7 +857,9 @@ def _get_media_mime_type_for_content_base(content: bytes) -> Optional[str]: # JPEG # - [0:2] is constant b"\xff\xd8" + # (ref. https://www.file-recovery.com/jpg-signature-format.htm) # - [2:4] Marker identifier b"\xff{?}" + # (ref. https://www.disktuna.com/list-of-jpeg-markers/) # NOTE: File ends with b"\xff\xd9" if content[0:3] == b"\xff\xd8\xff": return "image/jpeg" From f17c8081d4df524a632f69a73e42c86374ef0e46 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:23:04 +0200 Subject: [PATCH 022/506] change order of traits argument --- ayon_api/operations.py | 20 ++++++++++---------- ayon_api/server_api.py | 13 +++++++------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 701cf86e8..a77208df4 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -276,8 +276,8 @@ def new_representation_entity( tags=None, attribs=None, data=None, - entity_id=None, traits=None, + entity_id=None, ): """Create skeleton data of representation entity. @@ -291,10 +291,10 @@ def new_representation_entity( attribs (Optional[Dict[str, Any]]): Explicitly set attributes of representation. data (Optional[Dict[str, Any]]): Representation entity data. - entity_id (Optional[str]): Predefined id of entity. New id is created - if not passed. traits (Optional[Dict[str, Any]]): Representation traits. Empty if not passed. + entity_id (Optional[str]): Predefined id of entity. New id is created + if not passed. Returns: Dict[str, Any]: Skeleton of representation entity. @@ -1361,10 +1361,10 @@ def create_representation( attrib=None, data=None, tags=None, + traits=None, status=None, active=None, representation_id=None, - traits=None, ): """Create new representation. @@ -1376,12 +1376,12 @@ def create_representation( attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. tags (Optional[Iterable[str]]): Representation tags. + traits (Optional[Dict[str, Any]]): Representation traits. Empty + if not passed. status (Optional[str]): Representation status. active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not passed new id is generated. - traits (Optional[Dict[str, Any]]): Representation traits. Empty - if not passed. Returns: CreateOperation: Object of create operation. @@ -1399,9 +1399,9 @@ def create_representation( ("attrib", attrib), ("data", data), ("tags", tags), + ("traits", traits), ("status", status), ("active", active), - ("traits", traits), ): if value is not None: create_data[key] = value @@ -1422,9 +1422,9 @@ def update_representation( attrib=None, data=None, tags=None, + traits=None, status=None, active=None, - traits=None, ): """Update representation entity on server. @@ -1443,9 +1443,9 @@ def update_representation( attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. + traits (Optional[Dict[str, Any]]): New representation traits. status (Optional[str]): New status. active (Optional[bool]): New active state. - traits (Optional[Dict[str, Any]]): New representation traits. Returns: UpdateOperation: Object of update operation. @@ -1459,9 +1459,9 @@ def update_representation( ("attrib", attrib), ("data", data), ("tags", tags), + ("traits", traits), ("status", status), ("active", active), - ("traits", traits), ): if value is not None: update_data[key] = value diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e6034ec75..6b3d293cb 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7471,10 +7471,10 @@ def create_representation( attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, tags: Optional[List[str]]=None, + traits: Optional[Dict[str, Any]] = None, status: Optional[str] = None, active: Optional[bool] = None, representation_id: Optional[str] = None, - traits: Optional[Dict[str, Any]] = None, ) -> str: """Create new representation. @@ -7486,12 +7486,12 @@ def create_representation( attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. tags (Optional[Iterable[str]]): Representation tags. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. status (Optional[str]): Representation status. active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not passed new id is generated. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. Returns: str: Representation id. @@ -7508,10 +7508,10 @@ def create_representation( ("files", files), ("attrib", attrib), ("data", data), + ("traits", traits), ("tags", tags), ("status", status), ("active", active), - ("traits", traits), ): if value is not None: create_data[key] = value @@ -7533,9 +7533,9 @@ def update_representation( attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, tags: Optional[List[str]] = None, + traits: Optional[Dict[str, Any]] = None, status: Optional[str] = None, active: Optional[bool] = None, - traits: Optional[Dict[str, Any]] = None, ): """Update representation entity on server. @@ -7554,6 +7554,7 @@ def update_representation( attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. + traits (Optional[dict[str, Any]]): New traits. status (Optional[str]): New status. active (Optional[bool]): New active state. @@ -7566,9 +7567,9 @@ def update_representation( ("attrib", attrib), ("data", data), ("tags", tags), + ("traits", traits), ("status", status), ("active", active), - ("traits", traits), ): if value is not None: update_data[key] = value From adb3f8583d3fc12c1fb5dc623d2a694b047b3245 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:23:17 +0200 Subject: [PATCH 023/506] don't add traits if are empty --- ayon_api/operations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index a77208df4..f0572cdb2 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -313,10 +313,11 @@ def new_representation_entity( "name": name, "data": data, "attrib": attribs, - "traits": traits or {}, } if tags: output["tags"] = tags + if traits: + output["traits"] = traits if status: output["status"] = status return output From 24f279181a06654f7488d78bfa8ceb7bb7e59400 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 11 Apr 2025 16:23:32 +0200 Subject: [PATCH 024/506] rename 'grahql_allows_traits_in_representations' to 'representation_traits_available' --- ayon_api/server_api.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6b3d293cb..3bc699c37 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -521,7 +521,7 @@ def __init__( self._server_version_tuple = None self._graphql_allows_data_in_query = None - self._grahql_allows_traits_in_representations: Optional[bool] = None + self._representation_traits_available: Optional[bool] = None self._session = None @@ -1119,16 +1119,14 @@ def graphql_allows_data_in_query(self) -> bool: @property - def grahql_allows_traits_in_representations(self) -> bool: + def representation_traits_available(self) -> bool: """Check server support for representation traits.""" - if self._grahql_allows_traits_in_representations is None: + if self._representation_traits_available is None: major, minor, patch, _, _ = self.server_version_tuple - grahql_allows_traits_in_representations = True - if (major, minor, patch) < (1, 7, 5): - grahql_allows_traits_in_representations = False - self._grahql_allows_traits_in_representations = \ - grahql_allows_traits_in_representations - return self._grahql_allows_traits_in_representations + self._representation_traits_available = ( + (major, minor, patch) >= (1, 7, 5) + ) + return self._representation_traits_available def _get_user_info(self) -> Optional[Dict[str, Any]]: @@ -2776,7 +2774,7 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: if not self.graphql_allows_data_in_query: entity_type_defaults.discard("data") - if not self.grahql_allows_traits_in_representations: + if not self.representation_traits_available: entity_type_defaults.discard("traits") elif entity_type == "folderType": From 018dcd429db656833eb32c49fa7f9f28d5eaca2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:23:32 +0200 Subject: [PATCH 025/506] actually add traits to repre fields --- ayon_api/constants.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 9fd0f990a..93ff2877f 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -131,8 +131,7 @@ "data", "status", "tags", - # TODO (antirotor): traits should be there in time when server - # usage prior to 1.7.5 is not supported (used) anymore. + "traits", } REPRESENTATION_FILES_FIELDS = { From a3cd06c9873ba4123b2c0ff7ab0bb79259a6d17b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:23:46 +0200 Subject: [PATCH 026/506] reverse to 'graphql_allows_traits_in_representations' --- ayon_api/server_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 3bc699c37..eafff10e7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -521,7 +521,7 @@ def __init__( self._server_version_tuple = None self._graphql_allows_data_in_query = None - self._representation_traits_available: Optional[bool] = None + self._graphql_allows_traits_in_representations: Optional[bool] = None self._session = None @@ -1119,14 +1119,14 @@ def graphql_allows_data_in_query(self) -> bool: @property - def representation_traits_available(self) -> bool: + def graphql_allows_traits_in_representations(self) -> bool: """Check server support for representation traits.""" - if self._representation_traits_available is None: + if self._graphql_allows_traits_in_representations is None: major, minor, patch, _, _ = self.server_version_tuple - self._representation_traits_available = ( + self._graphql_allows_traits_in_representations = ( (major, minor, patch) >= (1, 7, 5) ) - return self._representation_traits_available + return self._graphql_allows_traits_in_representations def _get_user_info(self) -> Optional[Dict[str, Any]]: @@ -2774,7 +2774,7 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: if not self.graphql_allows_data_in_query: entity_type_defaults.discard("data") - if not self.representation_traits_available: + if not self.graphql_allows_traits_in_representations: entity_type_defaults.discard("traits") elif entity_type == "folderType": From a932482d227cac90183cec9ba58a5a3b3c27f698 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:24:45 +0200 Subject: [PATCH 027/506] update public api --- ayon_api/_api.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 1fc623d20..8d6715e88 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5473,10 +5473,10 @@ def create_representation( attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, tags: Optional[List[str]] = None, + traits: Optional[Dict[str, Any]] = None, status: Optional[str] = None, active: Optional[bool] = None, representation_id: Optional[str] = None, - traits: Optional[Dict[str, Any]] = None, ) -> str: """Create new representation. @@ -5488,12 +5488,12 @@ def create_representation( attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. tags (Optional[Iterable[str]]): Representation tags. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. status (Optional[str]): Representation status. active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not passed new id is generated. - traits (Optional[dict[str, Any]]): Representation traits - serialized as dict. Returns: str: Representation id. @@ -5508,10 +5508,10 @@ def create_representation( attrib=attrib, data=data, tags=tags, + traits=traits, status=status, active=active, representation_id=representation_id, - traits=traits, ) @@ -5524,9 +5524,9 @@ def update_representation( attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, tags: Optional[List[str]] = None, + traits: Optional[Dict[str, Any]] = None, status: Optional[str] = None, active: Optional[bool] = None, - traits: Optional[Dict[str, Any]] = None, ): """Update representation entity on server. @@ -5545,9 +5545,9 @@ def update_representation( attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. + traits (Optional[dict[str, Any]]): New traits. status (Optional[str]): New status. active (Optional[bool]): New active state. - traits (Optional[dict[str, Any]]): New traits. """ con = get_server_api_connection() @@ -5560,9 +5560,9 @@ def update_representation( attrib=attrib, data=data, tags=tags, + traits=traits, status=status, active=active, - traits=traits, ) From a919f7c7341ad48f29c740d2cd50b63c48904fbc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 09:30:02 +0200 Subject: [PATCH 028/506] move the 'traits' after 'data' --- ayon_api/_api.py | 12 ++++++------ ayon_api/operations.py | 16 ++++++++-------- ayon_api/server_api.py | 10 +++++----- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 8d6715e88..ca78be03d 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5472,8 +5472,8 @@ def create_representation( files: Optional[List[Dict[str, Any]]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, traits: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, representation_id: Optional[str] = None, @@ -5487,9 +5487,9 @@ def create_representation( files (Optional[list[dict]]): Representation files information. attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. - tags (Optional[Iterable[str]]): Representation tags. traits (Optional[dict[str, Any]]): Representation traits serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. status (Optional[str]): Representation status. active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not @@ -5507,8 +5507,8 @@ def create_representation( files=files, attrib=attrib, data=data, - tags=tags, traits=traits, + tags=tags, status=status, active=active, representation_id=representation_id, @@ -5523,8 +5523,8 @@ def update_representation( files: Optional[List[Dict[str, Any]]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, traits: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, ): @@ -5544,8 +5544,8 @@ def update_representation( information. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. traits (Optional[dict[str, Any]]): New traits. + tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. @@ -5559,8 +5559,8 @@ def update_representation( files=files, attrib=attrib, data=data, - tags=tags, traits=traits, + tags=tags, status=status, active=active, ) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 0e3db357c..d4383fda1 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -314,10 +314,10 @@ def new_representation_entity( "data": data, "attrib": attribs, } - if tags: - output["tags"] = tags if traits: output["traits"] = traits + if tags: + output["tags"] = tags if status: output["status"] = status return output @@ -1364,8 +1364,8 @@ def create_representation( files=None, attrib=None, data=None, - tags=None, traits=None, + tags=None, status=None, active=None, representation_id=None, @@ -1379,9 +1379,9 @@ def create_representation( files (Optional[list[dict]]): Representation files information. attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. - tags (Optional[Iterable[str]]): Representation tags. traits (Optional[Dict[str, Any]]): Representation traits. Empty if not passed. + tags (Optional[Iterable[str]]): Representation tags. status (Optional[str]): Representation status. active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not @@ -1402,8 +1402,8 @@ def create_representation( ("files", files), ("attrib", attrib), ("data", data), - ("tags", tags), ("traits", traits), + ("tags", tags), ("status", status), ("active", active), ): @@ -1425,8 +1425,8 @@ def update_representation( files=None, attrib=None, data=None, - tags=None, traits=None, + tags=None, status=None, active=None, ): @@ -1446,8 +1446,8 @@ def update_representation( information. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. traits (Optional[Dict[str, Any]]): New representation traits. + tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. @@ -1462,8 +1462,8 @@ def update_representation( ("files", files), ("attrib", attrib), ("data", data), - ("tags", tags), ("traits", traits), + ("tags", tags), ("status", status), ("active", active), ): diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index fb08ad520..e15ee5d5b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7472,8 +7472,8 @@ def create_representation( files: Optional[List[Dict[str, Any]]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]]=None, traits: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]]=None, status: Optional[str] = None, active: Optional[bool] = None, representation_id: Optional[str] = None, @@ -7487,9 +7487,9 @@ def create_representation( files (Optional[list[dict]]): Representation files information. attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. - tags (Optional[Iterable[str]]): Representation tags. traits (Optional[dict[str, Any]]): Representation traits serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. status (Optional[str]): Representation status. active (Optional[bool]): Representation active state. representation_id (Optional[str]): Representation id. If not @@ -7534,8 +7534,8 @@ def update_representation( files: Optional[List[Dict[str, Any]]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, traits: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, ): @@ -7555,8 +7555,8 @@ def update_representation( information. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. traits (Optional[dict[str, Any]]): New traits. + tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. @@ -7568,8 +7568,8 @@ def update_representation( ("files", files), ("attrib", attrib), ("data", data), - ("tags", tags), ("traits", traits), + ("tags", tags), ("status", status), ("active", active), ): From f431d4cf5f10ea43710199a1d7e555455a4271fe Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 13:22:48 +0200 Subject: [PATCH 029/506] fix endpoint for task method --- 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 6bd535a00..b1e286549 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7899,7 +7899,7 @@ def get_task_thumbnail( valid. """ - return self.get_thumbnail(project_name, "folder", task_id) + return self.get_thumbnail(project_name, "task", task_id) def get_version_thumbnail( self, From 6853defe4b1c1ff48a7079629c060706aef3fb19 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:28:44 +0200 Subject: [PATCH 030/506] remove 'graphql_allows_data_in_query' --- ayon_api/server_api.py | 102 ++++------------------------------------- 1 file changed, 8 insertions(+), 94 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 55608100b..e61e9ffd6 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -520,7 +520,6 @@ def __init__( self._server_version = None self._server_version_tuple = None - self._graphql_allows_data_in_query = None self._graphql_allows_traits_in_representations: Optional[bool] = None self._session = None @@ -1097,27 +1096,6 @@ def get_server_version_tuple(self) -> Tuple[int, int, int, str, str]: get_server_version_tuple ) - @property - def graphql_allows_data_in_query(self) -> bool: - """GraphQl query can support 'data' field. - - This applies only to project hierarchy entities 'project', 'folder', - 'task', 'product', 'version' and 'representation'. Others like 'user' - still require to use rest api to access 'data'. - - Returns: - bool: True if server supports 'data' field in GraphQl query. - - """ - if self._graphql_allows_data_in_query is None: - major, minor, patch, _, _ = self.server_version_tuple - graphql_allows_data_in_query = True - if (major, minor, patch) < (0, 5, 5): - graphql_allows_data_in_query = False - self._graphql_allows_data_in_query = graphql_allows_data_in_query - return self._graphql_allows_data_in_query - - @property def graphql_allows_traits_in_representations(self) -> bool: """Check server support for representation traits.""" @@ -2761,36 +2739,24 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: if entity_type == "project": entity_type_defaults = set(DEFAULT_PROJECT_FIELDS) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") elif entity_type == "folder": entity_type_defaults = set(DEFAULT_FOLDER_FIELDS) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") elif entity_type == "task": entity_type_defaults = set(DEFAULT_TASK_FIELDS) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") elif entity_type == "product": entity_type_defaults = set(DEFAULT_PRODUCT_FIELDS) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") elif entity_type == "version": entity_type_defaults = set(DEFAULT_VERSION_FIELDS) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") elif entity_type == "representation": entity_type_defaults = ( DEFAULT_REPRESENTATION_FIELDS | REPRESENTATION_FILES_FIELDS ) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") if not self.graphql_allows_traits_in_representations: entity_type_defaults.discard("traits") @@ -2806,8 +2772,6 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: elif entity_type == "workfile": entity_type_defaults = set(DEFAULT_WORKFILE_INFO_FIELDS) - if not self.graphql_allows_data_in_query: - entity_type_defaults.discard("data") elif entity_type == "user": entity_type_defaults = set(DEFAULT_USER_FIELDS) @@ -4968,15 +4932,10 @@ def get_folders( fields = set(fields) self._prepare_fields("folder", fields) - use_rest = False - if "data" in fields and not self.graphql_allows_data_in_query: - use_rest = True - fields = {"id"} - if active is not None: fields.add("active") - if own_attributes and not use_rest: + if own_attributes: fields.add("ownAttrib") query = folders_graphql_query(fields) @@ -5363,11 +5322,6 @@ def get_tasks( fields = set(fields) self._prepare_fields("task", fields, own_attributes) - use_rest = False - if "data" in fields and not self.graphql_allows_data_in_query: - use_rest = True - fields = {"id"} - if active is not None: fields.add("active") @@ -5380,10 +5334,7 @@ def get_tasks( if active is not None and active is not task["active"]: continue - if use_rest: - task = self.get_rest_task(project_name, task["id"]) - else: - self._convert_entity_data(task) + self._convert_entity_data(task) if own_attributes: fill_own_attribs(task) @@ -5525,11 +5476,6 @@ def get_tasks_by_folder_paths( fields = set(fields) self._prepare_fields("task", fields, own_attributes) - use_rest = False - if "data" in fields and not self.graphql_allows_data_in_query: - use_rest = True - fields = {"id"} - if active is not None: fields.add("active") @@ -5548,10 +5494,7 @@ def get_tasks_by_folder_paths( if active is not None and active is not task["active"]: continue - if use_rest: - task = self.get_rest_task(project_name, task["id"]) - else: - self._convert_entity_data(task) + self._convert_entity_data(task) if own_attributes: fill_own_attribs(task) @@ -5801,15 +5744,11 @@ def _filter_product( project_name: str, product: "ProductDict", active: "Union[bool, None]", - use_rest: bool, ) -> Optional["ProductDict"]: if active is not None and product["active"] is not active: return None - if use_rest: - product = self.get_rest_product(project_name, product["id"]) - else: - self._convert_entity_data(product) + self._convert_entity_data(product) return product @@ -5902,11 +5841,6 @@ def get_products( else: fields = self.get_default_fields_for_type("product") - use_rest = False - if "data" in fields and not self.graphql_allows_data_in_query: - use_rest = True - fields = {"id"} - if active is not None: fields.add("active") @@ -5964,7 +5898,7 @@ def get_products( products_by_folder_id = collections.defaultdict(list) for product in products: filtered_product = self._filter_product( - project_name, product, active, use_rest + project_name, product, active ) if filtered_product is not None: folder_id = filtered_product["folderId"] @@ -5978,7 +5912,7 @@ def get_products( else: for product in products: filtered_product = self._filter_product( - project_name, product, active, use_rest + project_name, product, active ) if filtered_product is not None: yield filtered_product @@ -6325,11 +6259,6 @@ def get_versions( # Make sure fields have minimum required fields fields |= {"id", "version"} - use_rest = False - if "data" in fields and not self.graphql_allows_data_in_query: - use_rest = True - fields = {"id"} - if active is not None: fields.add("active") @@ -6402,12 +6331,7 @@ def get_versions( if not hero and version["version"] < 0: continue - if use_rest: - version = self.get_rest_version( - project_name, version["id"] - ) - else: - self._convert_entity_data(version) + self._convert_entity_data(version) yield version @@ -6969,11 +6893,6 @@ def get_representations( fields = set(fields) self._prepare_fields("representation", fields) - use_rest = False - if "data" in fields and not self.graphql_allows_data_in_query: - use_rest = True - fields = {"id"} - if active is not None: fields.add("active") @@ -7055,12 +6974,7 @@ def get_representations( if active is not None and active is not repre["active"]: continue - if use_rest: - repre = self.get_rest_representation( - project_name, repre["id"] - ) - else: - self._convert_entity_data(repre) + self._convert_entity_data(repre) self._representation_conversion(repre) From 24be737e0516378c3fbf9de8886a6b31c0d49e4f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:28:58 +0200 Subject: [PATCH 031/506] remove progress and retries for enroll --- ayon_api/server_api.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e61e9ffd6..f1ef5ca06 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1665,21 +1665,6 @@ def update_event( ) if value is not None } - # 'progress' and 'retries' are available since 0.5.x server version - major, minor, _, _, _ = self.server_version_tuple - if (major, minor) < (0, 5): - args = [] - if progress is not None: - args.append("progress") - if retries is not None: - args.append("retries") - fields = ", ".join(f"'{f}'" for f in args) - ending = "s" if len(args) > 1 else "" - raise ValueError( - f"Your server version '{self.server_version}' does not" - f" support update of {fields} field{ending} on event." - " The fields are supported since server version '0.5'." - ) response = self.patch( f"events/{event_id}", From 19dc6f898d1ae00444e7cd1fc70881bb1ae24b59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:29:13 +0200 Subject: [PATCH 032/506] remove links compatibility --- ayon_api/server_api.py | 34 +--------------------------------- 1 file changed, 1 insertion(+), 33 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f1ef5ca06..04f4ff954 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -52,7 +52,6 @@ DEFAULT_EVENT_FIELDS, DEFAULT_ACTIVITY_FIELDS, DEFAULT_USER_FIELDS, - DEFAULT_LINK_FIELDS, ) from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( @@ -8340,22 +8339,8 @@ def create_link( kwargs = { "input": input_id, "output": output_id, + "linkType": full_link_type_name, } - major, minor, patch, rel, _ = self.server_version_tuple - rel_regex = re.compile(r"rc\.[0-5]") - if ( - ((major, minor, patch) == (1, 0, 0) and rel_regex.match(rel)) - or (major, minor, patch) < (1, 0, 0) - ): - kwargs["link"] = full_link_type_name - if link_name: - raise UnsupportedServerVersion( - "Link name is not supported" - f" for version of AYON server {self.server_version}" - ) - else: - kwargs["linkType"] = full_link_type_name - if link_name: kwargs["name"] = link_name @@ -8517,23 +8502,6 @@ def get_entities_links( return output link_fields = {"id", "links"} - # Backwards compatibility for server version 1.0.0-rc.5 and lower - # --------- - major, minor, patch, rel, _ = self.server_version_tuple - rel_regex = re.compile(r"rc\.[0-5]") - if ( - ((major, minor, patch) == (1, 0, 0) and rel_regex.match(rel)) - or (major, minor, patch) < (1, 0, 0) - ): - fields = set(DEFAULT_LINK_FIELDS) - fields.discard("name") - link_fields.discard("links") - link_fields |= { - f"links.{field}" - for field in fields - } - # --------- - query = query_func(link_fields) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) From 80526d14f41c3f694d441efbf7658f43126763e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:34:58 +0200 Subject: [PATCH 033/506] removed 'allow_data_changes' from entity hub --- ayon_api/entity_hub.py | 57 ++++++------------------------------------ 1 file changed, 7 insertions(+), 50 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 356caea63..d4d414f62 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -49,16 +49,10 @@ class EntityHub(object): Args: project_name (str): Name of project where changes will happen. connection (ServerAPI): Connection to server with logged user. - allow_data_changes (bool): This option gives ability to change 'data' - key on entities. This is not recommended as 'data' may be used for - secure information and would also slow down server queries. Content - of 'data' key can't be received only GraphQl. """ - def __init__( - self, project_name, connection=None, allow_data_changes=None - ): + def __init__(self, project_name, connection=None): if not connection: connection = get_server_api_connection() major, minor, _, _, _ = connection.server_version_tuple @@ -66,9 +60,6 @@ def __init__( if (major, minor) < (0, 6): path_start_with_slash = False - if allow_data_changes is None: - allow_data_changes = connection.graphql_allows_data_in_query - self._connection = connection self._path_start_with_slash = path_start_with_slash @@ -77,26 +68,8 @@ def __init__( self._entities_by_parent_id = collections.defaultdict(list) self._project_entity = UNKNOWN_VALUE - self._allow_data_changes = allow_data_changes - self._path_reset_queue = None - @property - def allow_data_changes(self): - """Entity hub allows changes of 'data' key on entities. - - Data are private and not all users may have access to them. - - Older version of AYON server allowed to get 'data' for entity only - using REST api calls, which means to query each entity on-by-one - from server. - - Returns: - bool: Data changes are allowed. - - """ - return self._allow_data_changes - @property def path_start_with_slash(self): """Folder path should start with slash. @@ -898,8 +871,7 @@ def _get_folder_fields(self) -> Set[str]: self._connection.get_default_fields_for_type("folder") ) folder_fields.add("hasProducts") - if self._allow_data_changes: - folder_fields.add("data") + folder_fields.add("data") return folder_fields def _get_task_fields(self) -> Set[str]: @@ -1717,10 +1689,7 @@ def _get_default_changes(self): """ changes = {} - if ( - self._entity_hub.allow_data_changes - and self._data is not UNKNOWN_VALUE - ): + if self._data is not UNKNOWN_VALUE: data_changes = self._data.get_changes() if data_changes: changes["data"] = data_changes @@ -3260,10 +3229,7 @@ def to_create_body_data(self): if self.thumbnail_id is not UNKNOWN_VALUE: output["thumbnailId"] = self.thumbnail_id - if ( - self._entity_hub.allow_data_changes - and self._data is not UNKNOWN_VALUE - ): + if self._data is not UNKNOWN_VALUE: output["data"] = self._data.get_new_entity_value() return output @@ -3451,10 +3417,7 @@ def to_create_body_data(self): if self.assignees: output["assignees"] = self.assignees - if ( - self._entity_hub.allow_data_changes - and self._data is not UNKNOWN_VALUE - ): + if self._data is not UNKNOWN_VALUE: output["data"] = self._data.get_new_entity_value() return output @@ -3561,10 +3524,7 @@ def to_create_body_data(self): if self.tags: output["tags"] = self.tags - if ( - self._entity_hub.allow_data_changes - and self._data is not UNKNOWN_VALUE - ): + if self._data is not UNKNOWN_VALUE: output["data"] = self._data.get_new_entity_value() return output @@ -3693,9 +3653,6 @@ def to_create_body_data(self): if self.status: output["status"] = self.status - if ( - self._entity_hub.allow_data_changes - and self._data is not UNKNOWN_VALUE - ): + if self._data is not UNKNOWN_VALUE: output["data"] = self._data.get_new_entity_value() return output From 3bcd4fd1f125227b95dc5ac6fd1a6193d2e8ae58 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:48:09 +0200 Subject: [PATCH 034/506] remove 'path_start_with_slash' from entity hub --- ayon_api/entity_hub.py | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index d4d414f62..ead6b4078 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -55,13 +55,8 @@ class EntityHub(object): def __init__(self, project_name, connection=None): if not connection: connection = get_server_api_connection() - major, minor, _, _, _ = connection.server_version_tuple - path_start_with_slash = True - if (major, minor) < (0, 6): - path_start_with_slash = False self._connection = connection - self._path_start_with_slash = path_start_with_slash self._project_name = project_name self._entities_by_id = {} @@ -70,18 +65,6 @@ def __init__(self, project_name, connection=None): self._path_reset_queue = None - @property - def path_start_with_slash(self): - """Folder path should start with slash. - - This changed in 0.6.x server version. - - Returns: - bool: Path starts with slash. - - """ - return self._path_start_with_slash - @property def project_name(self): """Project name which is maintained by hub. @@ -3118,10 +3101,8 @@ def get_path(self, dynamic_value=True): if parent.entity_type == "folder": parent_path = parent.path path = "/".join([parent_path, self.name]) - elif self._entity_hub.path_start_with_slash: - path = "/{}".format(self.name) else: - path = self.name + path = "/{}".format(self.name) self._path = path return self._path From a3c42a1d63101290896d951e5b1a54bf5d551292 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 14:49:47 +0200 Subject: [PATCH 035/506] remove forgotten 'use_rest' condition --- ayon_api/server_api.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 04f4ff954..bfdda4ca4 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4931,10 +4931,7 @@ def get_folders( if active is not None and active is not folder["active"]: continue - if use_rest: - folder = self.get_rest_folder(project_name, folder["id"]) - else: - self._convert_entity_data(folder) + self._convert_entity_data(folder) if own_attributes: fill_own_attribs(folder) From a317ef3497f30d8bb718c77ed0269db3d597993c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:49:33 +0200 Subject: [PATCH 036/506] bump version to '1.1.0' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 9902a6be7..adb43a85a 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.0.13-dev" +__version__ = "1.1.0" diff --git a/pyproject.toml b/pyproject.toml index b2de6967d..f9706bd19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.0.13-dev" +version = "1.1.0" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.0.13-dev" +version = "1.1.0" description = "AYON Python API" authors = [ "ynput.io " From 8578cdf89229822beed5bc83722c73a5620218ba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 14 Apr 2025 15:50:37 +0200 Subject: [PATCH 037/506] bump version to '1.1.1-dev.1' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index adb43a85a..0534782f8 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.0" +__version__ = "1.1.1-dev.1" diff --git a/pyproject.toml b/pyproject.toml index f9706bd19..d54b6cb6f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.0" +version = "1.1.1-dev.1" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.0" +version = "1.1.1-dev.1" description = "AYON Python API" authors = [ "ynput.io " From bf836447c8292876b65b5b21aeaf2d04b6441276 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:43:47 +0200 Subject: [PATCH 038/506] pass in 'referer' header --- ayon_api/server_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index bfdda4ca4..01385a5d0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1257,6 +1257,7 @@ def get_headers( "Content-Type": content_type, "x-ayon-platform": platform.system().lower(), "x-ayon-hostname": platform.node(), + "referer": self.get_base_url(), } if self._site_id is not None: headers["x-ayon-site-id"] = self._site_id From 48742015c793070fd88e701b00b1099b3884e1c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 May 2025 10:29:03 +0200 Subject: [PATCH 039/506] try connect handles change of schema --- ayon_api/utils.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 5529f121f..4758ce2fc 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -322,18 +322,38 @@ def _try_parse_url(url: str) -> Optional[str]: def _try_connect_to_server( - url: str, timeout: Optional[float] = None -) -> bool: + url: str, + timeout: Optional[float], + verify: Optional["Union[str, bool]"], + cert: Optional[str], +) -> Optional[str]: if timeout is None: timeout = get_default_timeout() + + if verify is None: + verify = os.environ.get("AYON_CA_FILE") or True + + if cert is None: + cert = os.environ.get("AYON_CERT_FILE") or None + try: # TODO add validation if the url lead to AYON server # - this won't validate if the url lead to 'google.com' - requests.get(url, timeout=timeout) + response = requests.get( + url, + timeout=timeout, + verify=verify, + cert=cert, + ) + if response.history: + return response.history[-1].headers["location"].rstrip("/") + return url - except BaseException: - return False - return True + except Exception: + print(f"Failed to connect to '{url}'") + traceback.print_exc() + + return None def login_to_server( From 16572503e446d47ed4eb28aa1a046ba1f6431c28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 May 2025 10:29:30 +0200 Subject: [PATCH 040/506] validate url can expect verify and cert arguments --- ayon_api/utils.py | 26 +++++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 4758ce2fc..dedf88356 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -483,7 +483,12 @@ def is_token_valid( return False -def validate_url(url: str, timeout: Optional[int] = None) -> str: +def validate_url( + url: str, + timeout: Optional[int] = None, + verify: Optional["Union[str, bool]"] = None, + cert: Optional[str] = None, +) -> str: """Validate url if is valid and server is available. Validation checks if can be parsed as url and contains scheme. @@ -540,12 +545,23 @@ def validate_url(url: str, timeout: Optional[int] = None) -> str: # Try add 'https://' scheme if is missing # - this will trigger UrlError if both will crash if not parsed_url.scheme: - new_url = "https://" + modified_url - if _try_connect_to_server(new_url, timeout=timeout): + new_url = _try_connect_to_server( + "http://" + modified_url, + timeout=timeout, + verify=verify, + cert=cert, + ) + if new_url: return new_url - if _try_connect_to_server(modified_url, timeout=timeout): - return modified_url + new_url = _try_connect_to_server( + modified_url, + timeout=timeout, + verify=verify, + cert=cert, + ) + if new_url: + return new_url hints = [] if "/" in parsed_url.path or not parsed_url.scheme: From a29ddfcc3cab3b526e59133142b5476e3c4924c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 May 2025 10:30:15 +0200 Subject: [PATCH 041/506] added missing import --- ayon_api/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index dedf88356..5055ebc2e 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -4,6 +4,7 @@ import uuid import string import platform +import traceback import collections from urllib.parse import urlparse, urlencode import typing From 2037ea8e9f4441e00d53b861341d8cc6aa4cfa0b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 May 2025 16:05:13 +0200 Subject: [PATCH 042/506] bump version to '1.1.1' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 0534782f8..379abd95f 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.1-dev.1" +__version__ = "1.1.1" diff --git a/pyproject.toml b/pyproject.toml index d54b6cb6f..995f0b24a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.1-dev.1" +version = "1.1.1" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.1-dev.1" +version = "1.1.1" description = "AYON Python API" authors = [ "ynput.io " From 4d124377c43224fde4b43dc30b6ed347015f94cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 2 May 2025 16:05:45 +0200 Subject: [PATCH 043/506] bump version to '1.1.2-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 379abd95f..4dd3d467c 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.1" +__version__ = "1.1.2-dev" diff --git a/pyproject.toml b/pyproject.toml index 995f0b24a..047bb0eaf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.1" +version = "1.1.2-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.1" +version = "1.1.2-dev" description = "AYON Python API" authors = [ "ynput.io " From f3a368e62a0e22707f98d697a4233819c120fc99 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 15:12:46 +0200 Subject: [PATCH 044/506] unidecode machine name for headers --- ayon_api/server_api.py | 3 ++- ayon_api/utils.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 01385a5d0..de09c5739 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -96,6 +96,7 @@ NOT_SET, get_media_mime_type, SortOrder, + get_machine_name, ) if typing.TYPE_CHECKING: @@ -1256,7 +1257,7 @@ def get_headers( headers = { "Content-Type": content_type, "x-ayon-platform": platform.system().lower(), - "x-ayon-hostname": platform.node(), + "x-ayon-hostname": get_machine_name(), "referer": self.get_base_url(), } if self._site_id is not None: diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 5055ebc2e..291583985 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -89,6 +89,17 @@ def get_default_settings_variant() -> str: return os.environ.get(DEFAULT_VARIANT_ENV_KEY) or "production" +def get_machine_name() -> str: + """Get machine name. + + Returns: + str: Machine name. + + """ + return platform.node() + return unidecode.unidecode(platform.node()) + + def get_default_site_id() -> Optional[str]: """Site id used for server connection. From ae1f9242e47c1ba7695f22f48296431ca7c5efc9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 15:16:10 +0200 Subject: [PATCH 045/506] convert accessGroups only if are string --- ayon_api/server_api.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 01385a5d0..09a302150 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1187,8 +1187,9 @@ def get_users( for parsed_data in query.continuous_query(self): for user in parsed_data["users"]: - user["accessGroups"] = json.loads( - user["accessGroups"]) + access_groups = user.get("accessGroups") + if isinstance(access_groups, str): + user["accessGroups"] = json.loads(access_groups) yield user def get_user_by_name( From 62e54d4984c844aba1a895cdb81cd6737041984e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 15:44:51 +0200 Subject: [PATCH 046/506] convert allAttrib too --- ayon_api/server_api.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 09a302150..79ca63f10 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1190,6 +1190,10 @@ def get_users( access_groups = user.get("accessGroups") if isinstance(access_groups, str): user["accessGroups"] = json.loads(access_groups) + all_attrib = user.get("allAttrib") + if isinstance(all_attrib, str): + user["allAttrib"] = json.loads(all_attrib) + fill_own_attribs(user) yield user def get_user_by_name( From c60094f09592b8abac7141ad8ddc1f88f3a531f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 15:45:00 +0200 Subject: [PATCH 047/506] fill own attribs on user --- ayon_api/server_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 79ca63f10..4c12dddb2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1250,7 +1250,9 @@ def get_user( response = self.get(f"users/{username}") response.raise_for_status() - return response.data + user = response.data + fill_own_attribs(user) + return user def get_headers( self, content_type: Optional[str] = None From 03aac80b6b4ce1f0c66bfd6380672d2a693af92d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 15:49:23 +0200 Subject: [PATCH 048/506] bump version to 1.1.2 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 4dd3d467c..0ef7f24e8 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.2-dev" +__version__ = "1.1.2" diff --git a/pyproject.toml b/pyproject.toml index 047bb0eaf..019bbac17 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.2-dev" +version = "1.1.2" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.2-dev" +version = "1.1.2" description = "AYON Python API" authors = [ "ynput.io " From 1276d476ccfc391559089930e52748cdd09b6b5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 15:50:01 +0200 Subject: [PATCH 049/506] bump version to '1.1.3-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 0ef7f24e8..1baff83b2 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.2" +__version__ = "1.1.3-dev" diff --git a/pyproject.toml b/pyproject.toml index 019bbac17..8cb72f2a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.2" +version = "1.1.3-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.2" +version = "1.1.3-dev" description = "AYON Python API" authors = [ "ynput.io " From b33d35c261e1b11aacd3a57bc3167ff9f793f3e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 16:09:03 +0200 Subject: [PATCH 050/506] remove redundant line --- ayon_api/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 291583985..6eb1941c7 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -96,7 +96,6 @@ def get_machine_name() -> str: str: Machine name. """ - return platform.node() return unidecode.unidecode(platform.node()) From 071bc2e7a890db5972d9ba600dbae08102132029 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 16:33:45 +0200 Subject: [PATCH 051/506] bump version to 1.1.3 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 1baff83b2..0706946ac 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.3-dev" +__version__ = "1.1.3" diff --git a/pyproject.toml b/pyproject.toml index 8cb72f2a4..67c7ac222 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.3-dev" +version = "1.1.3" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.3-dev" +version = "1.1.3" description = "AYON Python API" authors = [ "ynput.io " From 86a991c29bbe4f7eb9a69013ba5d688f1a0bab55 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 16:34:16 +0200 Subject: [PATCH 052/506] bump version to '1.1.4-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 0706946ac..212bc10f6 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.3" +__version__ = "1.1.4-dev" diff --git a/pyproject.toml b/pyproject.toml index 67c7ac222..e69d59c26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.3" +version = "1.1.4-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.3" +version = "1.1.4-dev" description = "AYON Python API" authors = [ "ynput.io " From 26d25dd2aedadf7eae817577e4b5f3e474f71e10 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 17:45:21 +0200 Subject: [PATCH 053/506] change project name to 'ayon_api' --- pyproject.toml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e69d59c26..5832e0e2d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "ayon-python-api" +name = "ayon_api" version = "1.1.4-dev" description = "AYON Python API" license = {file = "LICENSE"} @@ -27,7 +27,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -name = "ayon-python-api" +name = "ayon_api" version = "1.1.4-dev" description = "AYON Python API" authors = [ diff --git a/setup.py b/setup.py index fbcc88f92..87f7a462b 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ exec(open(VERSION_PATH).read(), _version_content) setup( - name="ayon-python-api", + name="ayon_api", version=_version_content["__version__"], py_modules=["ayon_api"], packages=["ayon_api"], From 2aab4ef76132ff1e8e210052fdd94c4223e42f55 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 19:05:06 +0200 Subject: [PATCH 054/506] small enhancements of publish CI action --- .github/workflows/python-publish.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index c2e199e3e..c2808e961 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -6,7 +6,7 @@ # separate terms of service, privacy policy, and support # documentation. -name: Upload Python Package +name: ⬆️ Upload Python Package on: release: @@ -17,8 +17,10 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/ayon-python-api steps: - uses: actions/checkout@v3 From 2e35b807c9420b58d84ddeab151082883f807c01 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 May 2025 19:05:36 +0200 Subject: [PATCH 055/506] change project name back to 'ayon-python-api' --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 5832e0e2d..e69d59c26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "ayon_api" +name = "ayon-python-api" version = "1.1.4-dev" description = "AYON Python API" license = {file = "LICENSE"} @@ -27,7 +27,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -name = "ayon_api" +name = "ayon-python-api" version = "1.1.4-dev" description = "AYON Python API" authors = [ From b8e1050106d405d8e09844825bfdbf5283c89647 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 30 May 2025 14:37:13 +0200 Subject: [PATCH 056/506] :sparkles: add support for product base types --- ayon_api/__init__.py | 3 + ayon_api/_api.py | 84 +++++++++++++++++- ayon_api/constants.py | 7 ++ ayon_api/entity_hub.py | 30 ++++++- ayon_api/graphql_queries.py | 48 +++++++++++ ayon_api/operations.py | 115 ++++++++++++++----------- ayon_api/server_api.py | 167 ++++++++++++++++++++++++++++++------ ayon_api/typing.py | 5 ++ 8 files changed, 374 insertions(+), 85 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 373cbad88..6c70e855f 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -181,6 +181,9 @@ get_product_types, get_project_product_types, get_product_type_names, + get_product_base_types, + get_project_product_base_types, + get_product_base_type_names, create_product, update_product, delete_product, diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 9c06b6f45..d6b47351f 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -65,6 +65,7 @@ FlatFolderDict, ProjectHierarchyDict, ProductTypeDict, + ProductBaseTypeDict, StreamType, ) @@ -4328,6 +4329,7 @@ def get_products( product_names: Optional[Iterable[str]] = None, folder_ids: Optional[Iterable[str]] = None, product_types: Optional[Iterable[str]] = None, + product_base_types: Optional[Iterable[str]] = None, product_name_regex: Optional[str] = None, product_path_regex: Optional[str] = None, names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, @@ -4337,21 +4339,22 @@ def get_products( fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, ) -> Generator["ProductDict", None, None]: - """Query products from server. + """Query products from the server. Todos: Separate 'name_by_folder_ids' filtering to separated method. It cannot be combined with some other filters. Args: - project_name (str): Name of project. + project_name (str): Name of the project. product_ids (Optional[Iterable[str]]): Task ids to filter. product_names (Optional[Iterable[str]]): Task names used for filtering. folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. + Use 'None' if folder is direct child of the project. product_types (Optional[Iterable[str]]): Product types used for filtering. + product_base_types (Optional[Iterable[str]]): Product base types product_name_regex (Optional[str]): Filter products by name regex. product_path_regex (Optional[str]): Filter products by path regex. Path starts with folder path and ends with product name. @@ -4380,6 +4383,7 @@ def get_products( product_names=product_names, folder_ids=folder_ids, product_types=product_types, + product_base_types=product_base_types, product_name_regex=product_name_regex, product_path_regex=product_path_regex, names_by_folder_ids=names_by_folder_ids, @@ -4461,7 +4465,7 @@ def get_product_types( ) -> List["ProductTypeDict"]: """Types of products. - This is server wide information. Product types have 'name', 'icon' and + This is the server-wide information. Product types have 'name', 'icon' and 'color'. Args: @@ -4477,6 +4481,27 @@ def get_product_types( ) +def get_product_base_types( + fields: Optional[Iterable[str]] = None, +) -> List["ProductBaseTypeDict"]: + """Base types of products. + + This is the server-wide information. Product base types have 'name', 'icon' + and 'color'. + + Args: + fields (Optional[Iterable[str]]): Product base types fields to query. + + Returns: + list[ProductBaseTypeDict]: Product base types information. + + """ + con = get_server_api_connection() + return con.get_product_base_types( + fields=fields, + ) + + def get_project_product_types( project_name: str, fields: Optional[Iterable[str]] = None, @@ -4501,6 +4526,30 @@ def get_project_product_types( ) +def get_project_product_base_types( + project_name: str, + fields: Optional[Iterable[str]] = None, +) -> List["ProductBaseTypeDict"]: + """Base types of products available in a project. + + Filter only product base types available in a project. + + Args: + project_name (str): Name of project where to look for + product base types. + fields (Optional[Iterable[str]]): Product base types fields to query. + + Returns: + List[ProductBaseTypeDict]: Product base types information. + + """ + con = get_server_api_connection() + return con.get_project_product_base_types( + project_name=project_name, + fields=fields, + ) + + def get_product_type_names( project_name: Optional[str] = None, product_ids: Optional[Iterable[str]] = None, @@ -4528,6 +4577,33 @@ def get_product_type_names( ) +def get_product_base_type_names( + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, +) -> Set[str]: + """Base product type names. + + Warnings: + Similar use case as `get_product_type_names` but for base + product types. + + Args: + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. + + Returns: + set[str]: Base product type names. + + """ + con = get_server_api_connection() + return con.get_product_base_type_names( + project_name=project_name, + product_ids=product_ids, + ) + + def create_product( project_name: str, name: str, diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 93ff2877f..312d3eb99 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -48,6 +48,13 @@ "color", } +# --- Product base type --- +DEFAULT_PRODUCT_BASE_TYPE_FIELDS = { + "name", + "icon", + "color", +} + # --- Project --- DEFAULT_PROJECT_FIELDS = { "active", diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index ead6b4078..7fa01e418 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -435,6 +435,7 @@ def add_new_product( self, name: str, product_type: str, + product_base_type: Optional[str] = None, folder_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, @@ -443,10 +444,11 @@ def add_new_product( entity_id: Optional[str] = None, created: Optional[bool] = True, ): - """Create task object and add it to entity hub. + """Create a task object and add it to the entity hub. Args: name (str): Name of entity. + product_base_type (str): Base type of product. product_type (str): Type of product. folder_id (Union[str, None]): Parent folder id. tags (Optional[Iterable[str]]): Folder tags. @@ -458,6 +460,11 @@ def add_new_product( created (Optional[bool]): Entity is new. When 'None' is passed the value is defined based on value of 'entity_id'. + Todo: + - Once the product base type is implemented and established, + it should be made mandatory to pass it and product_type + itself should be optional. + Returns: ProductEntity: Added product entity. @@ -465,6 +472,7 @@ def add_new_product( product_entity = ProductEntity( name=name, product_type=product_type, + product_base_type=product_base_type, folder_id=folder_id, tags=tags, attribs=attribs, @@ -3406,6 +3414,7 @@ def to_create_body_data(self): class ProductEntity(BaseEntity): _supports_name = True _supports_tags = True + _supports_base_type = True entity_type = "product" parent_entity_types = ["folder"] @@ -3414,6 +3423,7 @@ def __init__( self, name: str, product_type: str, + product_base_type: Optional[str] = None, folder_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, @@ -3435,6 +3445,7 @@ def __init__( entity_hub=entity_hub, ) self._product_type = product_type + self._product_base_type = product_base_type self._orig_product_type = product_type @@ -3454,6 +3465,21 @@ def set_product_type(self, product_type): product_type = property(get_product_type, set_product_type) + def get_product_base_type(self) -> Optional[str]: + """Get the product base type. + + Returns: + Optional[str]: The product base type, or None if not set. + + """ + return self._product_base_type + + def set_product_base_type(self, product_base_type: str) -> None: + """Set the product base type.""" + self._product_base_type = product_base_type + + product_base_type = property(get_product_base_type, set_product_base_type) + def lock(self): super().lock() self._orig_product_type = self._product_type @@ -3475,6 +3501,7 @@ def from_entity_data(cls, product, entity_hub): return cls( name=product["name"], product_type=product["productType"], + product_base_type=product["productBaseType"], folder_id=product["folderId"], tags=product["tags"], attribs=product["attrib"], @@ -3492,6 +3519,7 @@ def to_create_body_data(self): output = { "name": self.name, "productType": self.product_type, + "productBaseType": self.product_base_type, "folderId": self.parent_id, } diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 18b76f059..75938a7e9 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -119,6 +119,28 @@ def product_types_query(fields): return query +def product_base_types_query(fields): + query = GraphQlQuery("ProductBaseTypes") + product_base_types_field = query.add_field("productBaseTypes") + + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, product_base_types_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 + + def project_product_types_query(fields): query = GraphQlQuery("ProjectProductTypes") project_query = query.add_field("project") @@ -143,6 +165,30 @@ def project_product_types_query(fields): return query +def project_product_base_types_query(fields): + query = GraphQlQuery("ProjectProductBaseTypes") + project_query = query.add_field("project") + project_name_var = query.add_variable("projectName", "String!") + project_query.set_filter("name", project_name_var) + product_base_types_field = project_query.add_field("productBaseTypes") + nested_fields = fields_to_dict(fields) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, product_base_types_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 + + def folders_graphql_query(fields): query = GraphQlQuery("FoldersQuery") project_name_var = query.add_variable("projectName", "String!") @@ -298,6 +344,7 @@ def products_graphql_query(fields): product_names_var = query.add_variable("productNames", "[String!]") folder_ids_var = query.add_variable("folderIds", "[String!]") product_types_var = query.add_variable("productTypes", "[String!]") + product_base_types_var = query.add_variable("productBaseTypes", "[String!]") product_name_regex_var = query.add_variable("productNameRegex", "String!") product_path_regex_var = query.add_variable("productPathRegex", "String!") statuses_var = query.add_variable("productStatuses.", "[String!]") @@ -311,6 +358,7 @@ def products_graphql_query(fields): products_field.set_filter("names", product_names_var) products_field.set_filter("folderIds", folder_ids_var) products_field.set_filter("productTypes", product_types_var) + products_field.set_filter("productBaseTypes", product_base_types_var) products_field.set_filter("statuses", statuses_var) products_field.set_filter("tags", tags_var) products_field.set_filter("nameEx", product_name_regex_var) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index d4383fda1..bf977dace 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1,8 +1,10 @@ +from __future__ import annotations import os import copy import collections import uuid from abc import ABC, abstractmethod +from typing import Any, Iterable, Optional from ._api import get_server_api_connection from .utils import create_entity_id, REMOVED_VALUE, NOT_SET @@ -111,26 +113,28 @@ def new_folder_entity( def new_product_entity( - name, - product_type, - folder_id, - status=None, - tags=None, - attribs=None, - data=None, - entity_id=None -): - """Create skeleton data of product entity. + name: str, + produc_base_type: str, + product_type: str, + folder_id: str, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None +) -> dict[str, Any]: + """Create skeleton data of the product entity. Args: - name (str): Is considered as unique identifier of - product under folder. + name (str): Is considered as a unique identifier of + the product under the folder. + product_base_type (str): Base type of the product, e.g. "render", product_type (str): Product type. folder_id (str): Parent folder id. status (Optional[str]): Product status. tags (Optional[List[str]]): List of tags. attribs (Optional[Dict[str, Any]]): Explicitly set attributes - of product. + of the product. data (Optional[Dict[str, Any]]): product entity data. Empty dictionary is used if not passed. entity_id (Optional[str]): Predefined id of entity. New id is @@ -1090,22 +1094,24 @@ def delete_task(self, project_name, task_id): def create_product( self, - project_name, - name, - product_type, - folder_id, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - product_id=None, - ): - """Create new product. + project_name: str, + name: str, + product_base_type: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + product_id: Optional[str] = None, + ) -> CreateOperation: + """Create a new product. Args: project_name (str): Project name. name (str): Product name. + product_base_type (str): Base type of the product, e.g. "render", product_type (str): Product type. folder_id (str): Parent folder id. attrib (Optional[dict[str, Any]]): Product attributes. @@ -1125,6 +1131,7 @@ def create_product( create_data = { "id": product_id, "name": name, + "productBaseType": product_base_type, "productType": product_type, "folderId": folder_id, } @@ -1144,20 +1151,22 @@ def create_product( def update_product( self, - project_name, - product_id, - name=None, - folder_id=None, - product_type=None, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - ): + project_name: str, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_base_type: Optional[str] = None, + product_type: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ) -> UpdateOperation: """Update product entity on server. - Update of ``data`` will override existing value on folder entity. + Update of ``data`` will override the existing value on + the folder entity. Update of ``attrib`` does change only passed attributes. If you want to unset value, use ``None``. @@ -1167,6 +1176,7 @@ def update_product( product_id (str): Product id. name (Optional[str]): New product name. folder_id (Optional[str]): New product id. + product_base_type (Optional[str]): New product base type. product_type (Optional[str]): New product type. attrib (Optional[dict[str, Any]]): New product attributes. data (Optional[dict[str, Any]]): New product data. @@ -1178,20 +1188,21 @@ def update_product( UpdateOperation: Object of update operation. """ - update_data = {} - for key, value in ( - ("name", name), - ("productType", product_type), - ("folderId", folder_id), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - + update_data = { + key: value + for key, value in ( + ("name", name), + ("productBaseType", product_base_type), + ("productType", product_type), + ("folderId", folder_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ) + if value is not None + } return self.update_entity( project_name, "product", @@ -1200,7 +1211,7 @@ def update_product( ) def delete_product(self, project_name, product_id): - """Delete product. + """Delete the product. Args: project_name (str): Project name. diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 0409e9310..0d2714725 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -40,6 +40,7 @@ SERVER_RETRIES_ENV_KEY, DEFAULT_FOLDER_TYPE_FIELDS, DEFAULT_TASK_TYPE_FIELDS, + DEFAULT_PRODUCT_BASE_TYPE_FIELDS, DEFAULT_PRODUCT_TYPE_FIELDS, DEFAULT_PROJECT_FIELDS, DEFAULT_FOLDER_FIELDS, @@ -57,6 +58,7 @@ from .graphql_queries import ( project_graphql_query, projects_graphql_query, + project_product_base_types_query, project_product_types_query, product_types_query, folders_graphql_query, @@ -130,6 +132,7 @@ ProjectHierarchyDict, ProductTypeDict, + ProductBaseTypeDict, StreamType, ) @@ -2760,6 +2763,9 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: elif entity_type == "taskType": entity_type_defaults = set(DEFAULT_TASK_TYPE_FIELDS) + elif entity_type == "productBaseType": + entity_type_defaults = set(DEFAULT_PRODUCT_BASE_TYPE_FIELDS) + elif entity_type == "productType": entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS) @@ -5748,6 +5754,7 @@ def get_products( product_ids: Optional[Iterable[str]] = None, product_names: Optional[Iterable[str]]=None, folder_ids: Optional[Iterable[str]]=None, + product_base_types: Optional[Iterable[str]]=None, product_types: Optional[Iterable[str]]=None, product_name_regex: Optional[str] = None, product_path_regex: Optional[str] = None, @@ -5760,9 +5767,9 @@ def get_products( ) -> Generator["ProductDict", None, None]: """Query products from server. - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. + Todo: + - Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. Args: project_name (str): Name of project. @@ -5771,11 +5778,14 @@ def get_products( filtering. folder_ids (Optional[Iterable[str]]): Ids of task parents. Use 'None' if folder is direct child of project. + product_base_types (Optional[Iterable[str]]): Product base types + filtering. product_types (Optional[Iterable[str]]): Product types used for filtering. product_name_regex (Optional[str]): Filter products by name regex. product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. + Path starts with the folder path and ends with + the product name. names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product name filtering by folder id. statuses (Optional[Iterable[str]]): Product statuses used @@ -5785,7 +5795,7 @@ def get_products( active (Optional[bool]): Filter active/inactive products. Both are returned if is set to None. fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned + the folder. All possible folder fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for products. @@ -5863,6 +5873,7 @@ def get_products( if not _prepare_list_filters( filters, ("productIds", product_ids), + ("productBaseTypes", product_base_types), ("productTypes", product_types), ("productStatuses", statuses), ("productTags", tags), @@ -5947,7 +5958,7 @@ def get_product_by_name( product_name: str, folder_id: str, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER + own_attributes=_PLACEHOLDER, ) -> Optional["ProductDict"]: """Query product entity by name and folder id. @@ -5972,7 +5983,7 @@ def get_product_by_name( folder_ids=[folder_id], active=None, fields=fields, - own_attributes=own_attributes + own_attributes=own_attributes, ) for product in products: return product @@ -5983,8 +5994,8 @@ def get_product_types( ) -> List["ProductTypeDict"]: """Types of products. - This is server wide information. Product types have 'name', 'icon' and - 'color'. + This is the server-wide information. Product types have + 'name', 'icon' and 'color'. Args: fields (Optional[Iterable[str]]): Product types fields to query. @@ -6005,12 +6016,12 @@ def get_product_types( def get_project_product_types( self, project_name: str, fields: Optional[Iterable[str]] = None ) -> List["ProductTypeDict"]: - """Types of products available on a project. - - Filter only product types available on project. + """Types of products available in a project. + Filter only product types available in a project. +I Args: - project_name (str): Name of project where to look for + project_name (str): Name of the project where to look for product types. fields (Optional[Iterable[str]]): Product types fields to query. @@ -6068,12 +6079,102 @@ def get_product_type_names( ) } + def get_product_base_types( + self, fields: Optional[Iterable[str]] = None + ) -> List["ProductBaseTypeDict"]: + """Types of product base types. + + Args: + fields (Optional[Iterable[str]]): Product base types fields + to query. + + Returns: + list[ProductBaseTypeDict]: Product base types information. + + """ + if not fields: + fields = self.get_default_fields_for_type("productBaseType") + + query = product_types_query(fields) + + parsed_data = query.query(self) + + return parsed_data.get("productBaseTypes", []) + + + def get_project_product_base_types( + self, + project_name: str, + fields: Optional[Iterable[str]] = None + ) -> List["ProductBaseTypeDict"]: + """Product base types available in a project. + + Filter only product base types available in a project. + + Args: + project_name (str): Name of the project where to look for + product base types. + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + List[ProductBaseTypeDict]: Product Base types information. + + """ + if not fields: + fields = self.get_default_fields_for_type("productBaseType") + + query = project_product_base_types_query(fields) + query.set_variable_value("projectName", project_name) + + parsed_data = query.query(self) + + return parsed_data.get("project", {}).get("productBaseTypes", []) + + + def get_product_base_type_names( + self, + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, + ) -> Set[str]: + """Get projects roduct base type names. + + Args: + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. + + Returns: + set[str]: Product base type names used in the project. + + """ + if project_name and product_ids: + products = self.get_products( + project_name, + product_ids=product_ids, + fields=["productBaseType"], + active=None, + ) + return { + product["productBaseType"] + for product in products + } + + return { + product_info["name"] + for product_info in self.get_project_product_base_types( + project_name, fields=["name"] + ) + } + + def create_product( self, project_name: str, name: str, product_type: str, folder_id: str, + product_base_type: Optional[str] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, tags: Optional[Iterable[str]] =None, @@ -6081,13 +6182,14 @@ def create_product( active: "Union[bool, None]" = None, product_id: Optional[str] = None, ) -> str: - """Create new product. + """Create a new product. Args: project_name (str): Project name. name (str): Product name. product_type (str): Product type. folder_id (str): Parent folder id. + product_base_type (Optional[str]): Product base type. attrib (Optional[dict[str, Any]]): Product attributes. data (Optional[dict[str, Any]]): Product data. tags (Optional[Iterable[str]]): Product tags. @@ -6096,6 +6198,11 @@ def create_product( product_id (Optional[str]): Product id. If not passed new id is generated. + Todo: + - Once the product base type is implemented and established, + it should be made mandatory to pass it and product_type + itself should be optional. + Returns: str: Product id. @@ -6105,6 +6212,7 @@ def create_product( create_data = { "id": product_id, "name": name, + "productBaseType": product_base_type, "productType": product_type, "folderId": folder_id, } @@ -6131,6 +6239,7 @@ def update_product( product_id: str, name: Optional[str] = None, folder_id: Optional[str] = None, + product_base_type: Optional[str] = None, product_type: Optional[str] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, @@ -6150,6 +6259,7 @@ def update_product( product_id (str): Product id. name (Optional[str]): New product name. folder_id (Optional[str]): New product id. + product_base_type (Optional[str]): New product base type. product_type (Optional[str]): New product type. attrib (Optional[dict[str, Any]]): New product attributes. data (Optional[dict[str, Any]]): New product data. @@ -6158,20 +6268,21 @@ def update_product( active (Optional[bool]): New product active state. """ - update_data = {} - for key, value in ( - ("name", name), - ("productType", product_type), - ("folderId", folder_id), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - + update_data = { + key: value + for key, value in ( + ("name", name), + ("productBaseType", product_base_type), + ("productType", product_type), + ("folderId", folder_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ) + if value is not None + } response = self.patch( f"projects/{project_name}/products/{product_id}", **update_data diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 07f041aab..8e1e73364 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -352,4 +352,9 @@ class ProductTypeDict(TypedDict): icon: Optional[str] +class ProductBaseTypeDict(TypedDict): + name: str + color: Optional[str] + icon: Optional[str] + StreamType = Union[io.BytesIO, BinaryIO] From 1eeae553455ee956f737cf2a096838a6f6c26fe0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 30 May 2025 14:56:19 +0200 Subject: [PATCH 057/506] :bug: remove the stray character in comment --- 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 0d2714725..70e411425 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6019,7 +6019,7 @@ def get_project_product_types( """Types of products available in a project. Filter only product types available in a project. -I + Args: project_name (str): Name of the project where to look for product types. From 4d83ab80ae47b15df86149ae5fd2eb60bc27bb9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 13:52:56 +0200 Subject: [PATCH 058/506] :sparkles: add `productBaseType` to PRODUCT_FIELDS --- ayon_api/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 312d3eb99..3c6a9f6d7 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -105,6 +105,7 @@ "folderId", "active", "productType", + "productBaseType", "data", "status", "tags", From ceff641fd0f59a497d0e8e7d859af6138c526fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 13:11:31 +0200 Subject: [PATCH 059/506] :recycle: better handling of product base type in entity operations --- ayon_api/operations.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index bf977dace..8cdc2fbef 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -114,14 +114,14 @@ def new_folder_entity( def new_product_entity( name: str, - produc_base_type: str, product_type: str, folder_id: str, status: Optional[str] = None, tags: Optional[list[str]] = None, attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - entity_id: Optional[str] = None + entity_id: Optional[str] = None, + product_base_type: Optional[str] = None, ) -> dict[str, Any]: """Create skeleton data of the product entity. @@ -158,6 +158,9 @@ def new_product_entity( "data": data, "folderId": _create_or_convert_to_id(folder_id), } + if product_base_type: + output["productBaseType"] = product_base_type + if status: output["status"] = status if tags: @@ -1096,7 +1099,6 @@ def create_product( self, project_name: str, name: str, - product_base_type: str, product_type: str, folder_id: str, attrib: Optional[dict[str, Any]] = None, @@ -1105,6 +1107,7 @@ def create_product( status: Optional[str] = None, active: Optional[bool] = None, product_id: Optional[str] = None, + product_base_type: Optional[str] = None, ) -> CreateOperation: """Create a new product. @@ -1112,7 +1115,6 @@ def create_product( project_name (str): Project name. name (str): Product name. product_base_type (str): Base type of the product, e.g. "render", - product_type (str): Product type. folder_id (str): Parent folder id. attrib (Optional[dict[str, Any]]): Product attributes. data (Optional[dict[str, Any]]): Product data. @@ -1121,6 +1123,7 @@ def create_product( active (Optional[bool]): Product active state. product_id (Optional[str]): Product id. If not passed new id is generated. + product_base_type (Optional[str]): Product base type. Returns: CreateOperation: Object of create operation. @@ -1131,16 +1134,17 @@ def create_product( create_data = { "id": product_id, "name": name, - "productBaseType": product_base_type, "productType": product_type, "folderId": folder_id, } + for key, value in ( ("attrib", attrib), ("data", data), ("tags", tags), ("status", status), ("active", active), + ("productBaseType", product_base_type) ): if value is not None: create_data[key] = value From e3bc2c52f28c3048d8d9834994786af9337ff638 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 13:18:45 +0200 Subject: [PATCH 060/506] :sparkles: add product base type getters to init and linting --- ayon_api/__init__.py | 3 +++ ayon_api/graphql_queries.py | 3 ++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 6c70e855f..a2201ebc1 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -430,6 +430,9 @@ "get_product_types", "get_project_product_types", "get_product_type_names", + "get_product_base_types", + "get_project_product_base_types", + "get_product_base_type_names", "create_product", "update_product", "delete_product", diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 75938a7e9..e5cf26360 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -344,7 +344,8 @@ def products_graphql_query(fields): product_names_var = query.add_variable("productNames", "[String!]") folder_ids_var = query.add_variable("folderIds", "[String!]") product_types_var = query.add_variable("productTypes", "[String!]") - product_base_types_var = query.add_variable("productBaseTypes", "[String!]") + product_base_types_var = query.add_variable( + "productBaseTypes", "[String!]") product_name_regex_var = query.add_variable("productNameRegex", "String!") product_path_regex_var = query.add_variable("productPathRegex", "String!") statuses_var = query.add_variable("productStatuses.", "[String!]") From 3a96dd614a149b888fd779bb817d970bdd450450 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:47:11 +0200 Subject: [PATCH 061/506] fix used function to create query --- ayon_api/server_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 70e411425..a7e5b60f0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -61,6 +61,7 @@ project_product_base_types_query, project_product_types_query, product_types_query, + product_base_types_query, folders_graphql_query, tasks_graphql_query, tasks_by_folder_paths_graphql_query, @@ -6095,7 +6096,7 @@ def get_product_base_types( if not fields: fields = self.get_default_fields_for_type("productBaseType") - query = product_types_query(fields) + query = product_base_types_query(fields) parsed_data = query.query(self) From cb4a9f9045ceb1b587acd02e96ea7d969ae45d1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 11:51:20 +0200 Subject: [PATCH 062/506] remove unnecessary 'get_product_base_type_names' function --- ayon_api/server_api.py | 37 ------------------------------------- 1 file changed, 37 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a7e5b60f0..f3f70619b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6132,43 +6132,6 @@ def get_project_product_base_types( return parsed_data.get("project", {}).get("productBaseTypes", []) - def get_product_base_type_names( - self, - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, - ) -> Set[str]: - """Get projects roduct base type names. - - Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. - - Returns: - set[str]: Product base type names used in the project. - - """ - if project_name and product_ids: - products = self.get_products( - project_name, - product_ids=product_ids, - fields=["productBaseType"], - active=None, - ) - return { - product["productBaseType"] - for product in products - } - - return { - product_info["name"] - for product_info in self.get_project_product_base_types( - project_name, fields=["name"] - ) - } - - def create_product( self, project_name: str, From b963cd68aedd39cb28150a67704b005565a89a55 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:39:15 +0200 Subject: [PATCH 063/506] fix used method in 'get_product_type_names' --- ayon_api/server_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 0409e9310..e7c8063f7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6049,7 +6049,9 @@ def get_product_type_names( set[str]: Product type names. """ - if project_name and product_ids: + if project_name: + if not product_ids: + return set() products = self.get_products( project_name, product_ids=product_ids, @@ -6063,9 +6065,7 @@ def get_product_type_names( return { product_info["name"] - for product_info in self.get_project_product_types( - project_name, fields=["name"] - ) + for product_info in self.get_product_types(project_name) } def create_product( From 47934cc752072eb9790b22ae4fa43597af597fdb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:39:26 +0200 Subject: [PATCH 064/506] mark the method as deprecate --- ayon_api/server_api.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e7c8063f7..31a27e31f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6033,7 +6033,7 @@ def get_product_type_names( project_name: Optional[str] = None, product_ids: Optional[Iterable[str]] = None, ) -> Set[str]: - """Product type names. + """DEPRECATED Product type names. Warnings: This function will be probably removed. Matters if 'products_id' @@ -6049,6 +6049,12 @@ def get_product_type_names( set[str]: Product type names. """ + warnings.warn( + "Used deprecated function 'get_product_type_names'." + " Use 'get_product_types' or 'get_products' instead.", + DeprecationWarning, + stacklevel=2, + ) if project_name: if not product_ids: return set() From 3d2a22f0e7a4decc4877d8a0f1463dd3c58e5ef9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 15:39:41 +0200 Subject: [PATCH 065/506] added library to default project fields --- ayon_api/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 93ff2877f..f9273658b 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -51,6 +51,7 @@ # --- Project --- DEFAULT_PROJECT_FIELDS = { "active", + "library", "name", "code", "config", From 888f44308ffae4514275b2ed235f55b3f999d50d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:39:53 +0200 Subject: [PATCH 066/506] added more fields to default project fields --- ayon_api/constants.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index f9273658b..f44557d6d 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -56,10 +56,14 @@ "code", "config", "createdAt", + "updatedAt", "data", "folderTypes", "taskTypes", - "productTypes", + "linkTypes", + "statuses", + "tags", + "attrib", } # --- Folders --- From 9c4a4a1b5b411f8d05b6ecc057e83ce7e709f38a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:41:14 +0200 Subject: [PATCH 067/506] add productTypes to default fileds for newer server versions --- 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 31a27e31f..4e66af881 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -2732,6 +2732,9 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: if entity_type == "project": entity_type_defaults = set(DEFAULT_PROJECT_FIELDS) + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + entity_type_defaults.add("productTypes") elif entity_type == "folder": entity_type_defaults = set(DEFAULT_FOLDER_FIELDS) From 8dd85fb1ea13df4ee8f9716ea08d5a69d41152ac Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:41:36 +0200 Subject: [PATCH 068/506] projects query supports name filter --- ayon_api/graphql_queries.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 18b76f059..a5ba9547e 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -77,7 +77,9 @@ def project_graphql_query(fields): def projects_graphql_query(fields): query = GraphQlQuery("ProjectsQuery") + project_name_var = query.add_variable("projectName", "String!") projects_field = query.add_field_with_edges("projects") + projects_field.set_filter("name", project_name_var) nested_fields = fields_to_dict(fields) From ecd43567d3a3d714b0a16fb0bfaf0e8dcf98bccf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:41:52 +0200 Subject: [PATCH 069/506] added more default fileds for anatomy fields --- ayon_api/constants.py | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index f44557d6d..191d64219 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -30,17 +30,43 @@ "attrib.fullName", } -# --- Folder types --- +# --- Project folder types --- DEFAULT_FOLDER_TYPE_FIELDS = { "name", "icon", } -# --- Task types --- +# --- Project task types --- DEFAULT_TASK_TYPE_FIELDS = { "name", } +# --- Project tags --- +DEFAULT_PROJECT_TAGS_FIELDS = { + "name", + "color", +} + +# --- Project statuses --- +DEFAULT_PROJECT_STATUSES_FIELDS = { + "color", + "icon", + "name", + "scope", + "shortName", + "state", +} + +# --- Project link types --- +DEFAULT_PROJECT_LINK_TYPES_FIELDS = { + "color", + "inputType", + "linkType", + "name", + "outputType", + "style", +} + # --- Product types --- DEFAULT_PRODUCT_TYPE_FIELDS = { "name", From 641c5ecda5b1bb983779fbe06c092e98eb7d7036 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:45:47 +0200 Subject: [PATCH 070/506] added more logic related to how projects are fetched --- ayon_api/server_api.py | 33 ++++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 4e66af881..ca3be192e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4598,10 +4598,37 @@ def _should_use_rest_project( bool: REST endpoint must be used to get requested fields. """ - if fields is None: - return True + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + # Up to 1.10.0 some project data were not available in GraphQl. + # - 'config', 'tags', 'linkTypes' and 'statuses' at all + # - 'taskTypes', 'folderTypes' with only limited data + if (maj_v, min_v, patch_v) > (1, 10, 0): + return False + + for field in fields: + if ( + field.startswith("config") + or field.startswith("folderTypes") + or field.startswith("taskTypes") + or field.startswith("linkTypes") + or field.startswith("statuses") + or field.startswith("tags") + ): + return True + return False + + def _should_use_graphql_project( + self, fields: Optional[Iterable[str]] = None + ) -> bool: + """Fetch of project must be done using REST endpoint. + + Returns: + bool: REST endpoint must be used to get requested fields. + + """ for field in fields: - if field.startswith("config"): + # Product types are available only in GraphQl + if field.startswith("productTypes"): return True return False From 3bc6a74fa3b460d449692f7d8b80deb0c71b24fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:48:23 +0200 Subject: [PATCH 071/506] enhanced how projects are fetched --- ayon_api/server_api.py | 243 ++++++++++++++++++++++++++++------------- 1 file changed, 167 insertions(+), 76 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index ca3be192e..298ab624f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -40,6 +40,9 @@ SERVER_RETRIES_ENV_KEY, DEFAULT_FOLDER_TYPE_FIELDS, DEFAULT_TASK_TYPE_FIELDS, + DEFAULT_PROJECT_LINK_TYPES_FIELDS, + DEFAULT_PROJECT_STATUSES_FIELDS, + DEFAULT_PROJECT_TAGS_FIELDS, DEFAULT_PRODUCT_TYPE_FIELDS, DEFAULT_PROJECT_FIELDS, DEFAULT_FOLDER_FIELDS, @@ -2757,12 +2760,6 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: if not self.graphql_allows_traits_in_representations: entity_type_defaults.discard("traits") - elif entity_type == "folderType": - entity_type_defaults = set(DEFAULT_FOLDER_TYPE_FIELDS) - - elif entity_type == "taskType": - entity_type_defaults = set(DEFAULT_TASK_TYPE_FIELDS) - elif entity_type == "productType": entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS) @@ -4400,18 +4397,7 @@ def get_rest_project( if response.status != 200: return None project = response.data - # Add fake scope to statuses if not available - for status in project["statuses"]: - scope = status.get("scope") - if scope is None: - status["scope"] = [ - "folder", - "task", - "product", - "version", - "representation", - "workfile" - ] + self._fill_project_entity_data(project) return project def get_rest_projects( @@ -4436,6 +4422,7 @@ def get_rest_projects( for project_name in self.get_project_names(active, library): project = self.get_rest_project(project_name) if project: + self._fill_project_entity_data(project) yield project def get_rest_entity_by_id( @@ -4632,6 +4619,54 @@ def _should_use_graphql_project( return True return False + def _fill_project_entity_data(self, project): + # Add fake scope to statuses if not available + if "statuses" in project: + for status in project["statuses"]: + scope = status.get("scope") + if scope is None: + status["scope"] = [ + "folder", + "task", + "product", + "version", + "representation", + "workfile" + ] + + # Convert 'data' from string to dict if needed + if "data" in project: + project_data = project["data"] + if isinstance(project_data, str): + project_data = json.loads(project_data) + project["data"] = project_data + + # Fill 'bundle' from data if is not filled + if "bundle" not in project: + bundle_data = project["data"].get("bundle", {}) + prod_bundle = bundle_data.get("production") + staging_bundle = bundle_data.get("staging") + project["bundle"] = { + "production": prod_bundle, + "staging": staging_bundle, + } + + # Convert 'config' from string to dict if needed + config = project.get("config") + if isinstance(config, str): + project["config"] = json.loads(config) + + # Unifiy 'linkTypes' data structure from REST and GraphQL + if "linkTypes" in project: + for link_type in project["linkTypes"]: + if "data" in link_type: + link_data = link_type.pop("data") + link_type.update(link_data) + if "style" not in link_type: + link_type["style"] = None + if "color" not in link_type: + link_type["color"] = None + def get_projects( self, active: "Union[bool, None]" = True, @@ -4655,29 +4690,37 @@ def get_projects( Generator[ProjectDict, None, None]: Queried projects. """ - if fields is not None: - fields = set(fields) + if fields is None: + fields = self.get_default_fields_for_type("project") + + fields = set(fields) use_rest = self._should_use_rest_project(fields) - if use_rest: - for project in self.get_rest_projects(active, library): - if own_attributes: - fill_own_attribs(project) - yield project + use_graphql = self._should_use_graphql_project(fields) + if not use_rest: + yield from self._get_graphql_projects( + active, library, fields, own_attributes + ) return - self._prepare_fields("project", fields, own_attributes) - if active is not None: - fields.add("active") - - query = projects_graphql_query(fields) - for parsed_data in query.continuous_query(self): - for project in parsed_data["projects"]: - if active is not None and active is not project["active"]: - continue - if own_attributes: - fill_own_attribs(project) - yield project + p_by_name = {} + if use_graphql: + p_by_name = { + p["name"]: p + for p in self._get_graphql_projects( + active, + library, + fields={"name", "productTypes"}, + own_attributes=own_attributes, + ) + } + for project in self.get_rest_projects(active, library): + if own_attributes: + fill_own_attribs(project) + graphql_p = p_by_name.get(project["name"]) + if graphql_p: + project["productTypes"] = graphql_p["productTypes"] + yield project def get_project( self, @@ -4699,30 +4742,45 @@ def get_project( if project was not found. """ - if fields is not None: - fields = set(fields) + if fields is None: + fields = self.get_default_fields_for_type("project") - use_rest = self._should_use_rest_project(fields) - if use_rest: - project = self.get_rest_project(project_name) - if own_attributes: - fill_own_attribs(project) - return project - - self._prepare_fields("project", fields, own_attributes) - - query = project_graphql_query(fields) - query.set_variable_value("projectName", project_name) + fields = set(fields) - parsed_data = query.query(self) + use_rest = self._should_use_rest_project(fields) + use_graphql = self._should_use_graphql_project(fields) + if not use_rest: + for project in self._get_graphql_projects( + None, + None, + fields=fields, + own_attributes=own_attributes, + project_name=project_name, + ): + return project + return None - project = parsed_data["project"] - if project is not None: - project["name"] = project_name + p_by_name = {} + if use_graphql: + p_by_name = { + p["name"]: p + for p in self._get_graphql_projects( + None, + None, + fields={"name", "productTypes"}, + own_attributes=own_attributes, + project_name=project_name, + ) + } + for project in self.get_rest_projects(None, None): if own_attributes: fill_own_attribs(project) + graphql_p = p_by_name.get(project["name"]) + if graphql_p: + project["productTypes"] = graphql_p["productTypes"] + return project + return None - return project def get_folders_hierarchy( self, @@ -8964,29 +9022,62 @@ def _prepare_fields( if own_attributes and entity_type in {"project", "folder", "task"}: fields.add("ownAttrib") - if entity_type == "project": - if "folderTypes" in fields: - fields.remove("folderTypes") - fields |= { - f"folderTypes.{name}" - for name in self.get_default_fields_for_type("folderType") - } + if entity_type != "project": + return - if "taskTypes" in fields: - fields.remove("taskTypes") - fields |= { - f"taskTypes.{name}" - for name in self.get_default_fields_for_type("taskType") - } + # Use 'data' to fill 'bundle' data + if "bundle" in fields: + fields.remove("bundle") + fields.add("data") - if "productTypes" in fields: - fields.remove("productTypes") - fields |= { - f"productTypes.{name}" - for name in self.get_default_fields_for_type( - "productType" - ) - } + if "folderTypes" in fields: + fields.remove("folderTypes") + folder_types_fields = set(DEFAULT_FOLDER_TYPE_FIELDS) + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + folder_types_fields |= {"shortName"} + fields |= {f"folderTypes.{name}" for name in folder_types_fields} + + if "taskTypes" in fields: + fields.remove("taskTypes") + task_types_fields = set(DEFAULT_TASK_TYPE_FIELDS) + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + task_types_fields |= {"color", "icon", "shortName"} + fields |= {f"taskTypes.{name}" for name in task_types_fields} + + if "statuses" in fields: + fields.remove("statuses") + statuses_fields = set() + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + statuses_fields = set(DEFAULT_PROJECT_STATUSES_FIELDS) + fields |= {f"statuses.{name}" for name in statuses_fields} + + if "tags" in fields: + fields.remove("tags") + tags_fields = set() + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + tags_fields = set(DEFAULT_PROJECT_TAGS_FIELDS) + fields |= {f"tags.{name}" for name in tags_fields} + + if "linkTypes" in fields: + fields.remove("linkTypes") + link_types_fields = set() + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if (maj_v, min_v, patch_v) > (1, 10, 0): + link_types_fields = set(DEFAULT_PROJECT_LINK_TYPES_FIELDS) + fields |= {f"linkTypes.{name}" for name in link_types_fields} + + if "productTypes" in fields: + fields.remove("productTypes") + fields |= { + f"productTypes.{name}" + for name in self.get_default_fields_for_type( + "productType" + ) + } def _convert_entity_data(self, entity: "AnyEntityDict"): if not entity or "data" not in entity: From bf4b5d0a2c1bbe5b5a088220a2c2590da58c9767 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:50:39 +0200 Subject: [PATCH 072/506] implemented missing function to fetch projects using graphql --- ayon_api/server_api.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 298ab624f..c30344f75 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4781,6 +4781,34 @@ def get_project( return project return None + def _get_graphql_projects( + self, + active: Optional[bool], + library: Optional[bool], + fields: Set[str], + own_attributes: bool, + project_name: Optional[str] = None + ): + if active is not None: + fields.add("active") + + if library is not None: + fields.add("library") + + self._prepare_fields("project", fields, own_attributes) + + query = projects_graphql_query(fields) + if project_name is not None: + query.set_variable_value("projectName", project_name) + + for parsed_data in query.continuous_query(self): + for project in parsed_data["projects"]: + if active is not None and active is not project["active"]: + continue + if own_attributes: + fill_own_attribs(project) + self._fill_project_entity_data(project) + yield project def get_folders_hierarchy( self, From be5c4d89339da67b53d254146dae7a47b89fb3b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:50:57 +0200 Subject: [PATCH 073/506] deprecated 'get_project_product_types' --- ayon_api/graphql_queries.py | 24 ------------------------ ayon_api/server_api.py | 30 ++++++++++++++++++------------ 2 files changed, 18 insertions(+), 36 deletions(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index a5ba9547e..aef04730a 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -121,30 +121,6 @@ def product_types_query(fields): return query -def project_product_types_query(fields): - query = GraphQlQuery("ProjectProductTypes") - project_query = query.add_field("project") - project_name_var = query.add_variable("projectName", "String!") - project_query.set_filter("name", project_name_var) - product_types_field = project_query.add_field("productTypes") - nested_fields = fields_to_dict(fields) - - query_queue = collections.deque() - for key, value in nested_fields.items(): - query_queue.append((key, value, product_types_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 - - def folders_graphql_query(fields): query = GraphQlQuery("FoldersQuery") project_name_var = query.add_variable("projectName", "String!") diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index c30344f75..937781851 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -60,7 +60,6 @@ from .graphql_queries import ( project_graphql_query, projects_graphql_query, - project_product_types_query, product_types_query, folders_graphql_query, tasks_graphql_query, @@ -6121,12 +6120,12 @@ def get_product_types( def get_project_product_types( self, project_name: str, fields: Optional[Iterable[str]] = None ) -> List["ProductTypeDict"]: - """Types of products available on a project. + """DEPRECATED Types of products available in a project. - Filter only product types available on project. + Filter only product types available in a project. Args: - project_name (str): Name of project where to look for + project_name (str): Name of the project where to look for product types. fields (Optional[Iterable[str]]): Product types fields to query. @@ -6134,15 +6133,22 @@ def get_project_product_types( List[ProductTypeDict]: Product types information. """ - if not fields: - fields = self.get_default_fields_for_type("productType") - - query = project_product_types_query(fields) - query.set_variable_value("projectName", project_name) - - parsed_data = query.query(self) + warnings.warn( + "Used deprecated function 'get_project_product_types'." + " Use 'get_project' instead.", + DeprecationWarning, + stacklevel=2, + ) + if fields is None: + fields = {"productTypes"} + else: + fields = { + f"productTypes.{key}" + for key in fields + } - return parsed_data.get("project", {}).get("productTypes", []) + project = self.get_project(project_name, fields=fields) + return project["productTypes"] def get_product_type_names( self, From 4bba9f4a81b8ee9dc7021fb866d33616e39355f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:51:35 +0200 Subject: [PATCH 074/506] remove empty line --- ayon_api/server_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 937781851..f4c1ed236 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1108,7 +1108,6 @@ def graphql_allows_traits_in_representations(self) -> bool: ) return self._graphql_allows_traits_in_representations - def _get_user_info(self) -> Optional[Dict[str, Any]]: if self._access_token is None: return None From 54893cd4f930b80efaabe23004b8c3f91b9820c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:55:24 +0200 Subject: [PATCH 075/506] fix lines --- ayon_api/server_api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f4c1ed236..17049daf4 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -25,6 +25,7 @@ HTTPStatus = None import requests + try: # This should be used if 'requests' have it available from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError @@ -1588,7 +1589,6 @@ def get_events( ) statuses = states - filters = {} if not _prepare_list_filters( filters, @@ -1769,7 +1769,6 @@ def delete_event(self, event_id: str): response.raise_for_status() return response - def enroll_event_job( self, source_topic: "Union[str, List[str]]", @@ -9078,7 +9077,7 @@ def _prepare_fields( if (maj_v, min_v, patch_v) > (1, 10, 0): task_types_fields |= {"color", "icon", "shortName"} fields |= {f"taskTypes.{name}" for name in task_types_fields} - + if "statuses" in fields: fields.remove("statuses") statuses_fields = set() From 61d7994e27d478b4e5dc5353cd4a983a3badcd69 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 18:57:36 +0200 Subject: [PATCH 076/506] move functions around --- ayon_api/server_api.py | 182 ++++++++++++++++++++--------------------- 1 file changed, 91 insertions(+), 91 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 17049daf4..1dd208589 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4573,97 +4573,6 @@ def get_project_names( project_names.append(project["name"]) return project_names - def _should_use_rest_project( - self, fields: Optional[Iterable[str]] = None - ) -> bool: - """Fetch of project must be done using REST endpoint. - - Returns: - bool: REST endpoint must be used to get requested fields. - - """ - maj_v, min_v, patch_v, _, _ = self.server_version_tuple - # Up to 1.10.0 some project data were not available in GraphQl. - # - 'config', 'tags', 'linkTypes' and 'statuses' at all - # - 'taskTypes', 'folderTypes' with only limited data - if (maj_v, min_v, patch_v) > (1, 10, 0): - return False - - for field in fields: - if ( - field.startswith("config") - or field.startswith("folderTypes") - or field.startswith("taskTypes") - or field.startswith("linkTypes") - or field.startswith("statuses") - or field.startswith("tags") - ): - return True - return False - - def _should_use_graphql_project( - self, fields: Optional[Iterable[str]] = None - ) -> bool: - """Fetch of project must be done using REST endpoint. - - Returns: - bool: REST endpoint must be used to get requested fields. - - """ - for field in fields: - # Product types are available only in GraphQl - if field.startswith("productTypes"): - return True - return False - - def _fill_project_entity_data(self, project): - # Add fake scope to statuses if not available - if "statuses" in project: - for status in project["statuses"]: - scope = status.get("scope") - if scope is None: - status["scope"] = [ - "folder", - "task", - "product", - "version", - "representation", - "workfile" - ] - - # Convert 'data' from string to dict if needed - if "data" in project: - project_data = project["data"] - if isinstance(project_data, str): - project_data = json.loads(project_data) - project["data"] = project_data - - # Fill 'bundle' from data if is not filled - if "bundle" not in project: - bundle_data = project["data"].get("bundle", {}) - prod_bundle = bundle_data.get("production") - staging_bundle = bundle_data.get("staging") - project["bundle"] = { - "production": prod_bundle, - "staging": staging_bundle, - } - - # Convert 'config' from string to dict if needed - config = project.get("config") - if isinstance(config, str): - project["config"] = json.loads(config) - - # Unifiy 'linkTypes' data structure from REST and GraphQL - if "linkTypes" in project: - for link_type in project["linkTypes"]: - if "data" in link_type: - link_data = link_type.pop("data") - link_type.update(link_data) - if "style" not in link_type: - link_type["style"] = None - if "color" not in link_type: - link_type["color"] = None - def get_projects( self, active: "Union[bool, None]" = True, @@ -4778,6 +4687,97 @@ def get_project( return project return None + def _should_use_rest_project( + self, fields: Optional[Iterable[str]] = None + ) -> bool: + """Fetch of project must be done using REST endpoint. + + Returns: + bool: REST endpoint must be used to get requested fields. + + """ + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + # Up to 1.10.0 some project data were not available in GraphQl. + # - 'config', 'tags', 'linkTypes' and 'statuses' at all + # - 'taskTypes', 'folderTypes' with only limited data + if (maj_v, min_v, patch_v) > (1, 10, 0): + return False + + for field in fields: + if ( + field.startswith("config") + or field.startswith("folderTypes") + or field.startswith("taskTypes") + or field.startswith("linkTypes") + or field.startswith("statuses") + or field.startswith("tags") + ): + return True + return False + + def _should_use_graphql_project( + self, fields: Optional[Iterable[str]] = None + ) -> bool: + """Fetch of project must be done using REST endpoint. + + Returns: + bool: REST endpoint must be used to get requested fields. + + """ + for field in fields: + # Product types are available only in GraphQl + if field.startswith("productTypes"): + return True + return False + + def _fill_project_entity_data(self, project: Dict[str, Any]) -> None: + # Add fake scope to statuses if not available + if "statuses" in project: + for status in project["statuses"]: + scope = status.get("scope") + if scope is None: + status["scope"] = [ + "folder", + "task", + "product", + "version", + "representation", + "workfile" + ] + + # Convert 'data' from string to dict if needed + if "data" in project: + project_data = project["data"] + if isinstance(project_data, str): + project_data = json.loads(project_data) + project["data"] = project_data + + # Fill 'bundle' from data if is not filled + if "bundle" not in project: + bundle_data = project["data"].get("bundle", {}) + prod_bundle = bundle_data.get("production") + staging_bundle = bundle_data.get("staging") + project["bundle"] = { + "production": prod_bundle, + "staging": staging_bundle, + } + + # Convert 'config' from string to dict if needed + config = project.get("config") + if isinstance(config, str): + project["config"] = json.loads(config) + + # Unifiy 'linkTypes' data structure from REST and GraphQL + if "linkTypes" in project: + for link_type in project["linkTypes"]: + if "data" in link_type: + link_data = link_type.pop("data") + link_type.update(link_data) + if "style" not in link_type: + link_type["style"] = None + if "color" not in link_type: + link_type["color"] = None + def _get_graphql_projects( self, active: Optional[bool], From 0e5a9c261319c3fb43635ee27519530a02509075 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:00:37 +0200 Subject: [PATCH 077/506] updated public api --- ayon_api/_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 9c06b6f45..23e9769aa 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4481,12 +4481,12 @@ def get_project_product_types( project_name: str, fields: Optional[Iterable[str]] = None, ) -> List["ProductTypeDict"]: - """Types of products available on a project. + """DEPRECATED Types of products available in a project. - Filter only product types available on project. + Filter only product types available in a project. Args: - project_name (str): Name of project where to look for + project_name (str): Name of the project where to look for product types. fields (Optional[Iterable[str]]): Product types fields to query. @@ -4505,7 +4505,7 @@ def get_product_type_names( project_name: Optional[str] = None, product_ids: Optional[Iterable[str]] = None, ) -> Set[str]: - """Product type names. + """DEPRECATED Product type names. Warnings: This function will be probably removed. Matters if 'products_id' From 0a3af2f4cce241cbeca062e8e15149156023dbcf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 26 Jun 2025 19:40:38 +0200 Subject: [PATCH 078/506] simplified projects fetching --- ayon_api/server_api.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 1dd208589..af12ef9f6 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4419,7 +4419,6 @@ def get_rest_projects( for project_name in self.get_project_names(active, library): project = self.get_rest_project(project_name) if project: - self._fill_project_entity_data(project) yield project def get_rest_entity_by_id( @@ -4609,23 +4608,23 @@ def get_projects( ) return - p_by_name = {} if use_graphql: - p_by_name = { - p["name"]: p - for p in self._get_graphql_projects( - active, - library, - fields={"name", "productTypes"}, - own_attributes=own_attributes, - ) - } + for graphql_project in self._get_graphql_projects( + active, + library, + fields={"name", "productTypes"}, + own_attributes=own_attributes, + ): + project = self.get_project(graphql_project["name"]) + if own_attributes: + fill_own_attribs(project) + project["productTypes"] = graphql_project["productTypes"] + yield project + return + for project in self.get_rest_projects(active, library): if own_attributes: fill_own_attribs(project) - graphql_p = p_by_name.get(project["name"]) - if graphql_p: - project["productTypes"] = graphql_p["productTypes"] yield project def get_project( From 3c9d131cd9ff69135e648d42531f1f2170db9d91 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:13:11 +0200 Subject: [PATCH 079/506] use rest as primary getter for project --- ayon_api/server_api.py | 143 +++++++++++++++-------------------------- 1 file changed, 53 insertions(+), 90 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index af12ef9f6..50f991a3f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -4595,36 +4595,28 @@ def get_projects( Generator[ProjectDict, None, None]: Queried projects. """ - if fields is None: - fields = self.get_default_fields_for_type("project") - - fields = set(fields) - - use_rest = self._should_use_rest_project(fields) - use_graphql = self._should_use_graphql_project(fields) - if not use_rest: - yield from self._get_graphql_projects( - active, library, fields, own_attributes - ) - return + if fields is not None: + fields = set(fields) - if use_graphql: - for graphql_project in self._get_graphql_projects( + graphql_fields, use_rest = self._get_project_graphql_fields(fields) + projects_by_name = {} + if graphql_fields: + projects = list(self._get_graphql_projects( active, library, - fields={"name", "productTypes"}, + fields=graphql_fields, own_attributes=own_attributes, - ): - project = self.get_project(graphql_project["name"]) - if own_attributes: - fill_own_attribs(project) - project["productTypes"] = graphql_project["productTypes"] - yield project - return + )) + if not use_rest: + yield from projects + return + projects_by_name = {p["name"]: p for p in projects} for project in self.get_rest_projects(active, library): - if own_attributes: - fill_own_attribs(project) + name = project["name"] + graphql_p = projects_by_name.get(name) + if graphql_p: + project["productTypes"] = graphql_p["productTypes"] yield project def get_project( @@ -4647,87 +4639,58 @@ def get_project( if project was not found. """ - if fields is None: - fields = self.get_default_fields_for_type("project") - - fields = set(fields) + if fields is not None: + fields = set(fields) - use_rest = self._should_use_rest_project(fields) - use_graphql = self._should_use_graphql_project(fields) - if not use_rest: - for project in self._get_graphql_projects( + graphql_fields, use_rest = self._get_project_graphql_fields(fields) + graphql_project = None + if graphql_fields: + graphql_project = next(self._get_graphql_projects( None, None, - fields=fields, + fields=graphql_fields, own_attributes=own_attributes, - project_name=project_name, - ): - return project - return None + ), None) + if not graphql_project or not use_rest: + return graphql_project - p_by_name = {} - if use_graphql: - p_by_name = { - p["name"]: p - for p in self._get_graphql_projects( - None, - None, - fields={"name", "productTypes"}, - own_attributes=own_attributes, - project_name=project_name, - ) - } - for project in self.get_rest_projects(None, None): - if own_attributes: - fill_own_attribs(project) - graphql_p = p_by_name.get(project["name"]) - if graphql_p: - project["productTypes"] = graphql_p["productTypes"] - return project - return None + project = self.get_rest_project(project_name) + if own_attributes: + fill_own_attribs(project) + if graphql_project: + project["productTypes"] = graphql_project["productTypes"] + return project - def _should_use_rest_project( - self, fields: Optional[Iterable[str]] = None - ) -> bool: + def _get_project_graphql_fields( + self, fields: Optional[Set[str]] + ) -> Tuple[Set[str], bool]: """Fetch of project must be done using REST endpoint. Returns: - bool: REST endpoint must be used to get requested fields. + set[str]: GraphQl fields. """ - maj_v, min_v, patch_v, _, _ = self.server_version_tuple - # Up to 1.10.0 some project data were not available in GraphQl. - # - 'config', 'tags', 'linkTypes' and 'statuses' at all - # - 'taskTypes', 'folderTypes' with only limited data - if (maj_v, min_v, patch_v) > (1, 10, 0): - return False - - for field in fields: - if ( - field.startswith("config") - or field.startswith("folderTypes") - or field.startswith("taskTypes") - or field.startswith("linkTypes") - or field.startswith("statuses") - or field.startswith("tags") - ): - return True - return False - - def _should_use_graphql_project( - self, fields: Optional[Iterable[str]] = None - ) -> bool: - """Fetch of project must be done using REST endpoint. - - Returns: - bool: REST endpoint must be used to get requested fields. + if fields is None: + return set(), True - """ + has_product_types = False + graphql_fields = set() for field in fields: # Product types are available only in GraphQl if field.startswith("productTypes"): - return True - return False + has_product_types = True + graphql_fields.add(field) + + if not has_product_types: + return set(), True + + inters = fields & {"name", "code", "active", "library"} + remainders = fields - (inters | graphql_fields) + if remainders: + graphql_fields.add("name") + return graphql_fields, True + graphql_fields |= inters + return graphql_fields, False def _fill_project_entity_data(self, project: Dict[str, Any]) -> None: # Add fake scope to statuses if not available From 61f5ce887cdad7cf0c9855b753368844362b26dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 10:53:36 +0200 Subject: [PATCH 080/506] more specific message --- 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 50f991a3f..c6a11f41e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6095,7 +6095,7 @@ def get_project_product_types( """ warnings.warn( "Used deprecated function 'get_project_product_types'." - " Use 'get_project' instead.", + " Use 'get_project' with 'productTypes' in 'fields' instead.", DeprecationWarning, stacklevel=2, ) From 786af014a11fb030da7823c821d0394bcc2f9a31 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:03:51 +0200 Subject: [PATCH 081/506] merge same logic into one loop --- ayon_api/server_api.py | 36 +++++++++++------------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index c6a11f41e..8ebe8aca0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -9024,10 +9024,10 @@ def _prepare_fields( fields.remove("bundle") fields.add("data") + maj_v, min_v, patch_v, _, _ = self.server_version_tuple if "folderTypes" in fields: fields.remove("folderTypes") folder_types_fields = set(DEFAULT_FOLDER_TYPE_FIELDS) - maj_v, min_v, patch_v, _, _ = self.server_version_tuple if (maj_v, min_v, patch_v) > (1, 10, 0): folder_types_fields |= {"shortName"} fields |= {f"folderTypes.{name}" for name in folder_types_fields} @@ -9035,34 +9035,20 @@ def _prepare_fields( if "taskTypes" in fields: fields.remove("taskTypes") task_types_fields = set(DEFAULT_TASK_TYPE_FIELDS) - maj_v, min_v, patch_v, _, _ = self.server_version_tuple if (maj_v, min_v, patch_v) > (1, 10, 0): task_types_fields |= {"color", "icon", "shortName"} fields |= {f"taskTypes.{name}" for name in task_types_fields} - if "statuses" in fields: - fields.remove("statuses") - statuses_fields = set() - maj_v, min_v, patch_v, _, _ = self.server_version_tuple - if (maj_v, min_v, patch_v) > (1, 10, 0): - statuses_fields = set(DEFAULT_PROJECT_STATUSES_FIELDS) - fields |= {f"statuses.{name}" for name in statuses_fields} - - if "tags" in fields: - fields.remove("tags") - tags_fields = set() - maj_v, min_v, patch_v, _, _ = self.server_version_tuple - if (maj_v, min_v, patch_v) > (1, 10, 0): - tags_fields = set(DEFAULT_PROJECT_TAGS_FIELDS) - fields |= {f"tags.{name}" for name in tags_fields} - - if "linkTypes" in fields: - fields.remove("linkTypes") - link_types_fields = set() - maj_v, min_v, patch_v, _, _ = self.server_version_tuple - if (maj_v, min_v, patch_v) > (1, 10, 0): - link_types_fields = set(DEFAULT_PROJECT_LINK_TYPES_FIELDS) - fields |= {f"linkTypes.{name}" for name in link_types_fields} + for field, default_fields in ( + ("statuses", DEFAULT_PROJECT_STATUSES_FIELDS), + ("tags", DEFAULT_PROJECT_TAGS_FIELDS), + ("linkTypes", DEFAULT_PROJECT_TAGS_FIELDS), + ): + if (maj_v, min_v, patch_v) <= (1, 10, 0): + break + if field in fields: + fields.remove(field) + fields |= {f"{field}.{name}" for name in default_fields} if "productTypes" in fields: fields.remove("productTypes") From cdda9cd9035597987dedc71605e321f5c4bc84bd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Jun 2025 12:05:30 +0200 Subject: [PATCH 082/506] remove unused imports --- ayon_api/server_api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 8ebe8aca0..a6d5aa249 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -41,7 +41,6 @@ SERVER_RETRIES_ENV_KEY, DEFAULT_FOLDER_TYPE_FIELDS, DEFAULT_TASK_TYPE_FIELDS, - DEFAULT_PROJECT_LINK_TYPES_FIELDS, DEFAULT_PROJECT_STATUSES_FIELDS, DEFAULT_PROJECT_TAGS_FIELDS, DEFAULT_PRODUCT_TYPE_FIELDS, @@ -59,7 +58,6 @@ ) from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( - project_graphql_query, projects_graphql_query, product_types_query, folders_graphql_query, From 3a1dd63f9a17cd7e37b0670f43649d60ed9acfd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:41:51 +0200 Subject: [PATCH 083/506] added constnas and typing helpers for entity lists --- ayon_api/constants.py | 18 ++++++++++++++++++ ayon_api/typing.py | 20 ++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 191d64219..d47e4fc59 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -227,3 +227,21 @@ "entityType", "author.name", } + + +DEFAULT_ENTITY_LIST_FIELDS = { + "id", + "count", + "attributes", + "active", + "createdBy", + "createdAt", + "entityListType", + "data", + "entityType", + "label", + "owner", + "tags", + "updatedAt", + "updatedBy", +} diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 07f041aab..9c84489c2 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -28,6 +28,21 @@ "watching", ] +EntityListEntityType = Literal[ + "folder", + "product", + "version", + "representation", + "task", + "workfile", +] + +EntityListItemMode = Literal[ + "replace", + "merge", + "delete", +] + EventFilterValueType = Union[ None, str, int, float, @@ -353,3 +368,8 @@ class ProductTypeDict(TypedDict): StreamType = Union[io.BytesIO, BinaryIO] + + +class EntityListAttributeDefinitionDict(TypedDict): + name: str + data: Dict[str, Any] From fed13c387ac9bc6a7111b467f975173d4c5cbd21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:42:46 +0200 Subject: [PATCH 084/506] implemented entity list methods --- ayon_api/graphql_queries.py | 30 +++ ayon_api/server_api.py | 406 ++++++++++++++++++++++++++++++++++++ 2 files changed, 436 insertions(+) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index aef04730a..8973ee12e 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -665,3 +665,33 @@ def activities_graphql_query(fields, order): query_queue.append((k, v, field)) return query + + +def entity_lists_graphql_query(fields): + query = GraphQlQuery("EntityLists") + project_name_var = query.add_variable("projectName", "String!") + entity_list_ids = query.add_variable("listIds", "String!") + + project_field = query.add_field("project") + project_field.set_filter("name", project_name_var) + + entity_lists_field = project_field.add_field_with_edges("entityLists") + entity_lists_field.set_filter("ids", entity_list_ids) + + nested_fields = fields_to_dict(set(fields)) + + query_queue = collections.deque() + for key, value in nested_fields.items(): + query_queue.append((key, value, entity_lists_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 a6d5aa249..818409bd2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -55,6 +55,7 @@ DEFAULT_EVENT_FIELDS, DEFAULT_ACTIVITY_FIELDS, DEFAULT_USER_FIELDS, + DEFAULT_ENTITY_LIST_FIELDS, ) from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( @@ -71,6 +72,7 @@ events_graphql_query, users_graphql_query, activities_graphql_query, + entity_lists_graphql_query, ) from .exceptions import ( FailedOperations, @@ -105,6 +107,8 @@ from .typing import ( ActivityType, ActivityReferenceType, + EntityListEntityType, + EntityListItemMode, LinkDirection, EventFilter, AttributeScope, @@ -132,6 +136,7 @@ ProjectHierarchyDict, ProductTypeDict, StreamType, + EntityListAttributeDefinitionDict, ) PatternType = type(re.compile("")) @@ -2764,6 +2769,9 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: elif entity_type == "user": entity_type_defaults = set(DEFAULT_USER_FIELDS) + elif entity_type == "entityList": + entity_type_defaults = set(DEFAULT_ENTITY_LIST_FIELDS) + else: raise ValueError(f"Unknown entity type \"{entity_type}\"") return ( @@ -8854,6 +8862,404 @@ def get_representation_links( project_name, [representation_id], link_types, link_direction )[representation_id] + def get_entity_lists( + self, + project_name: str, + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + ) -> Generator[Dict[str, Any], None, None]: + """Fetch entity lists from server. + + Args: + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Generator[Dict[str, Any], None, None]: Entity list entities + matching defined filters. + + """ + if fields is None: + fields = self.get_default_fields_for_type("entityList") + fields = set(fields) + + if active is not None: + fields.add("active") + + filters = {"projectName": project_name} + if list_ids is not None: + if not list_ids: + return + filters["listIds"] = list(set(list_ids)) + + query = entity_lists_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for entity_list in parsed_data["project"]["entityLists"]: + if active is not None and entity_list["active"] != active: + continue + + attributes = entity_list.get("attributes") + if isinstance(attributes, str): + entity_list["attributes"] = json.loads(attributes) + + self._convert_entity_data(entity_list) + + yield entity_list + + def get_entity_list_rest( + self, project_name: str, list_id: str + ) -> Optional[Dict[str, Any]]: + """Get entity list by id using REST API. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + response = self.get(f"projects/{project_name}/lists/{list_id}") + response.raise_for_status() + return response.data + + def get_entity_list_by_id( + self, + project_name: str, + list_id: str, + fields: Optional[Iterable[str]] = None, + ) -> Optional[Dict[str, Any]]: + """Get entity list by id using GraphQl. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + for entity_list in self.get_entity_lists( + project_name, list_ids=[list_id], active=None, fields=fields + ): + return entity_list + return None + + def create_entity_list( + self, + project_name: str, + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + template: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[List[Dict[str, Any]]] = None, + list_id: Optional[str] = None, + ) -> str: + """Create entity list. + + Args: + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. + + """ + if list_id is None: + list_id = create_entity_id() + kwargs = { + "id": list_id, + "entityType": entity_type, + "label": label, + } + for key, value in ( + ("entityListType", list_type), + ("access", access), + ("attrib", attrib), + ("template", template), + ("tags", tags), + ("owner", owner), + ("data", data), + ("active", active), + ("items", items), + ): + if value is not None: + kwargs[key] = value + + response = self.post( + f"projects/{project_name}/lists/{list_id}/items", + **kwargs + + ) + response.raise_for_status() + return list_id + + def update_entity_list( + self, + project_name: str, + list_id: str, + *, + label: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + ) -> None: + """Update entity list. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + + """ + kwargs = { + key: value + for key, value in ( + ("label", label), + ("access", access), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("owner", owner), + ("active", active), + ) + if value is not None + } + response = self.patch( + f"projects/{project_name}/lists/{list_id}", + **kwargs + ) + response.raise_for_status() + + def delete_entity_list(self, project_name: str, list_id: str) -> None: + """Delete entity list from project. + + Args: + project_name (str): Project name. + list_id (str): Entity list id that will be removed. + + """ + response = self.delete(f"projects/{project_name}/lists/{list_id}") + response.raise_for_status() + + def get_entity_list_attribute_definitions( + self, project_name: str, list_id: str + ) -> List["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + List[EntityListAttributeDefinitionDict]: List of attribute + definitions. + + """ + response = self.get( + f"projects/{project_name}/lists/{list_id}/attributes" + ) + response.raise_for_status() + return response.data + + def set_entity_list_attribute_definitions( + self, + project_name: str, + list_id: str, + attribute_definitions: List["EntityListAttributeDefinitionDict"], + ) -> None: + """Set attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (List[EntityListAttributeDefinitionDict]): + List of attribute definitions. + + """ + response = self.raw_put( + f"projects/{project_name}/lists/{list_id}/attributes", + json=attribute_definitions, + ) + response.raise_for_status() + + def create_entity_list_item( + self, + project_name: str, + list_id: str, + *, + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + item_id: Optional[str] = None, + ) -> str: + """Create entity list item. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. + + Returns: + str: Item id. + + """ + if item_id is None: + item_id = create_entity_id() + kwargs = { + "id": item_id, + "entityId": list_id, + } + for key, value in ( + ("position", position), + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ): + if value is not None: + kwargs[key] = value + + response = self.post( + f"projects/{project_name}/lists/{list_id}/items", + **kwargs + ) + response.raise_for_status() + return item_id + + def update_entity_list_items( + self, + project_name: str, + list_id: str, + items: List[Dict[str, Any]], + mode: "EntityListItemMode", + ) -> None: + """Update items in entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (List[Dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. + + """ + response = self.post( + f"projects/{project_name}/lists/{list_id}/items", + items=items, + mode=mode, + ) + response.raise_for_status() + + def update_entity_list_item( + self, + project_name: str, + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + ) -> None: + """Update item in entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. + + """ + kwargs = {} + for key, value in ( + ("entityId", new_list_id), + ("position", position), + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ): + if value is not None: + kwargs[key] = value + response = self.patch( + f"projects/{project_name}/lists/{list_id}/items/{item_id}", + **kwargs, + ) + response.raise_for_status() + + def delete_entity_list_item( + self, + project_name: str, + list_id: str, + item_id: str, + ) -> None: + """Delete item from entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. + + """ + response = self.delete( + f"projects/{project_name}/lists/{list_id}/items/{item_id}", + ) + response.raise_for_status() + # --- Batch operations processing --- def send_batch_operations( self, From d234dc15bdf475f0d06d94fa553fa2deb2dcfc7c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:42:59 +0200 Subject: [PATCH 085/506] modified automated api --- automated_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/automated_api.py b/automated_api.py index 329c6c858..55b0cb594 100644 --- a/automated_api.py +++ b/automated_api.py @@ -249,6 +249,9 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): body_params.append(f"*{var_positional}") func_params.append(f"*{var_positional}") + elif kw_only: + func_params.append(f"*") + for param_name, param in kw_only: body_params.append(f"{param_name}={param_name}") func_params.append(_kw_default_to_str(param_name, param, api_globals)) From 46e1d1d6cd4bbff17ecc03323b8df06ac9a64cb4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 30 Jun 2025 16:43:13 +0200 Subject: [PATCH 086/506] created public functions for entity lists --- ayon_api/__init__.py | 24 +++ ayon_api/_api.py | 361 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 385 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 373cbad88..1d6c0eeb5 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -241,6 +241,18 @@ get_version_links, get_representations_links, get_representation_links, + get_entity_lists, + get_entity_list_rest, + get_entity_list_by_id, + create_entity_list, + update_entity_list, + delete_entity_list, + get_entity_list_attribute_definitions, + set_entity_list_attribute_definitions, + create_entity_list_item, + update_entity_list_items, + update_entity_list_item, + delete_entity_list_item, send_batch_operations, send_activities_batch_operations, ) @@ -487,6 +499,18 @@ "get_version_links", "get_representations_links", "get_representation_links", + "get_entity_lists", + "get_entity_list_rest", + "get_entity_list_by_id", + "create_entity_list", + "update_entity_list", + "delete_entity_list", + "get_entity_list_attribute_definitions", + "set_entity_list_attribute_definitions", + "create_entity_list_item", + "update_entity_list_items", + "update_entity_list_item", + "delete_entity_list_item", "send_batch_operations", "send_activities_batch_operations", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 23e9769aa..bfbc77748 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6646,6 +6646,367 @@ def get_representation_links( ) +def get_entity_lists( + project_name: str, + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, +) -> Generator[Dict[str, Any], None, None]: + """Fetch entity lists from server. + + Args: + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Generator[Dict[str, Any], None, None]: Entity list entities + matching defined filters. + + """ + con = get_server_api_connection() + return con.get_entity_lists( + project_name=project_name, + list_ids=list_ids, + active=active, + fields=fields, + ) + + +def get_entity_list_rest( + project_name: str, + list_id: str, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using REST API. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + con = get_server_api_connection() + return con.get_entity_list_rest( + project_name=project_name, + list_id=list_id, + ) + + +def get_entity_list_by_id( + project_name: str, + list_id: str, + fields: Optional[Iterable[str]] = None, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using GraphQl. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + con = get_server_api_connection() + return con.get_entity_list_by_id( + project_name=project_name, + list_id=list_id, + fields=fields, + ) + + +def create_entity_list( + project_name: str, + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + template: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[List[Dict[str, Any]]] = None, + list_id: Optional[str] = None, +) -> str: + """Create entity list. + + Args: + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. + + """ + con = get_server_api_connection() + return con.create_entity_list( + project_name=project_name, + entity_type=entity_type, + label=label, + list_type=list_type, + access=access, + attrib=attrib, + data=data, + tags=tags, + template=template, + owner=owner, + active=active, + items=items, + list_id=list_id, + ) + + +def update_entity_list( + project_name: str, + list_id: str, + *, + label: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, +) -> None: + """Update entity list. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + + """ + con = get_server_api_connection() + return con.update_entity_list( + project_name=project_name, + list_id=list_id, + label=label, + access=access, + attrib=attrib, + data=data, + tags=tags, + owner=owner, + active=active, + ) + + +def delete_entity_list( + project_name: str, + list_id: str, +) -> None: + """Delete entity list from project. + + Args: + project_name (str): Project name. + list_id (str): Entity list id that will be removed. + + """ + con = get_server_api_connection() + return con.delete_entity_list( + project_name=project_name, + list_id=list_id, + ) + + +def get_entity_list_attribute_definitions( + project_name: str, + list_id: str, +) -> List["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + List[EntityListAttributeDefinitionDict]: List of attribute + definitions. + + """ + con = get_server_api_connection() + return con.get_entity_list_attribute_definitions( + project_name=project_name, + list_id=list_id, + ) + + +def set_entity_list_attribute_definitions( + project_name: str, + list_id: str, + attribute_definitions: List["EntityListAttributeDefinitionDict"], +) -> None: + """Set attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (List[EntityListAttributeDefinitionDict]): + List of attribute definitions. + + """ + con = get_server_api_connection() + return con.set_entity_list_attribute_definitions( + project_name=project_name, + list_id=list_id, + attribute_definitions=attribute_definitions, + ) + + +def create_entity_list_item( + project_name: str, + list_id: str, + *, + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + item_id: Optional[str] = None, +) -> str: + """Create entity list item. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. + + Returns: + str: Item id. + + """ + con = get_server_api_connection() + return con.create_entity_list_item( + project_name=project_name, + list_id=list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + item_id=item_id, + ) + + +def update_entity_list_items( + project_name: str, + list_id: str, + items: List[Dict[str, Any]], + mode: "EntityListItemMode", +) -> None: + """Update items in entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (List[Dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. + + """ + con = get_server_api_connection() + return con.update_entity_list_items( + project_name=project_name, + list_id=list_id, + items=items, + mode=mode, + ) + + +def update_entity_list_item( + project_name: str, + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, +) -> None: + """Update item in entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. + + """ + con = get_server_api_connection() + return con.update_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, + new_list_id=new_list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + ) + + +def delete_entity_list_item( + project_name: str, + list_id: str, + item_id: str, +) -> None: + """Delete item from entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. + + """ + con = get_server_api_connection() + return con.delete_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, + ) + + def send_batch_operations( project_name: str, operations: List[Dict[str, Any]], From e3b9134b1a65c6c3fb164d6f7f032856776aa18f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:22:24 +0200 Subject: [PATCH 087/506] fix url path to delete representation --- 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 a6d5aa249..6a47b8c23 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -7601,7 +7601,7 @@ def delete_representation( """ response = self.delete( - f"projects/{project_name}/representation/{representation_id}" + f"projects/{project_name}/representations/{representation_id}" ) response.raise_for_status() From 094f25b9fce4989cab474eabc1d89064bbb0839b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 24 Jul 2025 12:22:33 +0200 Subject: [PATCH 088/506] fix type in operations --- ayon_api/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index d4383fda1..8a9dac465 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1489,5 +1489,5 @@ def delete_representation(self, project_name, representation_id): """ return self.delete_entity( - project_name, "representaion", representation_id + project_name, "representation", representation_id ) From df474b3f254b467b3cf9b449d0f6f8fc6f914ff7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:32:25 +0200 Subject: [PATCH 089/506] add missing import --- ayon_api/_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index bfbc77748..3749390b9 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -41,6 +41,8 @@ from .typing import ( ActivityType, ActivityReferenceType, + EntityListEntityType, + EntityListItemMode, LinkDirection, EventFilter, AttributeScope, @@ -66,6 +68,7 @@ ProjectHierarchyDict, ProductTypeDict, StreamType, + EntityListAttributeDefinitionDict, ) From ee25f39c93061b8cf63b4036c8413194372bc2a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 14:38:17 +0200 Subject: [PATCH 090/506] remove unnecessary f-string --- automated_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/automated_api.py b/automated_api.py index 55b0cb594..74c936f66 100644 --- a/automated_api.py +++ b/automated_api.py @@ -250,7 +250,7 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): func_params.append(f"*{var_positional}") elif kw_only: - func_params.append(f"*") + func_params.append("*") for param_name, param in kw_only: body_params.append(f"{param_name}={param_name}") From 2db4b1019a5f957c8953ba94a261c90d788a066f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:00:17 +0200 Subject: [PATCH 091/506] do no fill own attribs in graphql --- ayon_api/server_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6a47b8c23..2f8f61432 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1194,7 +1194,6 @@ def get_users( all_attrib = user.get("allAttrib") if isinstance(all_attrib, str): user["allAttrib"] = json.loads(all_attrib) - fill_own_attribs(user) yield user def get_user_by_name( From 7bc569916ed882b60801812e4927918fa3642338 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:08:09 +0200 Subject: [PATCH 092/506] added commented way how to fill attributes for future reference --- ayon_api/server_api.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 2f8f61432..c1725e261 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1232,7 +1232,7 @@ def get_user_by_name( def get_user( self, username: Optional[str] = None ) -> Optional[Dict[str, Any]]: - """Get user info using REST endpoit. + """Get user info using REST endpoint. Args: username (Optional[str]): Username. @@ -1243,14 +1243,18 @@ def get_user( """ if username is None: - output = self._get_user_info() - if output is None: + user = self._get_user_info() + if user is None: raise UnauthorizedError("User is not authorized.") - return output + else: + response = self.get(f"users/{username}") + response.raise_for_status() + user = response.data + + # NOTE This would fill all missing attributes with 'None' + # for attr_name in self.get_attributes_for_type("user"): + # user["attrib"].setdefault(attr_name, None) - response = self.get(f"users/{username}") - response.raise_for_status() - user = response.data fill_own_attribs(user) return user From ac4001a860054dc45e6afda4f3b5d629d68f0d6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:33:49 +0200 Subject: [PATCH 093/506] use default attribute value for None values --- ayon_api/server_api.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index c1725e261..82db6cddc 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1186,6 +1186,7 @@ def get_users( for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) + attributes = self.get_attributes_for_type("user") for parsed_data in query.continuous_query(self): for user in parsed_data["users"]: access_groups = user.get("accessGroups") @@ -1194,6 +1195,15 @@ def get_users( all_attrib = user.get("allAttrib") if isinstance(all_attrib, str): user["allAttrib"] = json.loads(all_attrib) + if "attrib" in user: + user["ownAttrib"] = user["attrib"].copy() + attrib = user["attrib"] + for key, value in tuple(attrib.items()): + if value is not None: + continue + attr_def = attributes.get(key) + if attr_def is not None: + attrib[key] = attr_def["default"] yield user def get_user_by_name( From 589ac3d151830fe3d5cf07370a00407e7b560717 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:34:01 +0200 Subject: [PATCH 094/506] added more information to comments or docstring --- ayon_api/server_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 82db6cddc..ce0462c22 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1243,6 +1243,8 @@ def get_user( self, username: Optional[str] = None ) -> Optional[Dict[str, Any]]: """Get user info using REST endpoint. + + User contains only explicitly set attributes in 'attrib'. Args: username (Optional[str]): Username. @@ -1261,7 +1263,8 @@ def get_user( response.raise_for_status() user = response.data - # NOTE This would fill all missing attributes with 'None' + # NOTE Server does return only filled attributes right now. + # This would fill all missing attributes with 'None'. # for attr_name in self.get_attributes_for_type("user"): # user["attrib"].setdefault(attr_name, None) From a9a9bdfeb624205a20490c8a2b0e9c5c408ffe36 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:34:21 +0200 Subject: [PATCH 095/506] added check for "ownAttrib" key --- 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 ce0462c22..5bb3342e8 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -321,7 +321,7 @@ def __repr__(self): def fill_own_attribs(entity): - if not entity or not entity.get("attrib"): + if not entity or not entity.get("attrib") or "ownAttrib" not in entity: return attributes = set(entity["ownAttrib"]) From 4ebad5f2aa7b06d8d55c9a4bc5341571b8c879b4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:35:11 +0200 Subject: [PATCH 096/506] faster check --- ayon_api/server_api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 5bb3342e8..3edbb9d86 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -321,10 +321,13 @@ def __repr__(self): def fill_own_attribs(entity): - if not entity or not entity.get("attrib") or "ownAttrib" not in entity: + if not entity or not entity.get("attrib"): return - attributes = set(entity["ownAttrib"]) + attributes = entity.get("ownAttrib") + if attributes is None: + return + attributes = set(attributes) own_attrib = {} entity["ownAttrib"] = own_attrib From 3dfa2f04061adbc43af8b8f056280cd4ea994eab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:38:02 +0200 Subject: [PATCH 097/506] update public api docstring --- ayon_api/_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 23e9769aa..0bfe3ee63 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -766,7 +766,9 @@ def get_user_by_name( def get_user( username: Optional[str] = None, ) -> Optional[Dict[str, Any]]: - """Get user info using REST endpoit. + """Get user info using REST endpoint. + + User contains only explicitly set attributes in 'attrib'. Args: username (Optional[str]): Username. From b6d74b36fb7641abf06140a02116ac3a39bb24f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:38:45 +0200 Subject: [PATCH 098/506] fix formatting --- 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 3edbb9d86..c1b3e7f97 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1246,7 +1246,7 @@ def get_user( self, username: Optional[str] = None ) -> Optional[Dict[str, Any]]: """Get user info using REST endpoint. - + User contains only explicitly set attributes in 'attrib'. Args: From aa23fe540fa62e074820f71b69b44443c66c680f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 15:42:32 +0200 Subject: [PATCH 099/506] fix variable names in products query --- ayon_api/graphql_queries.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index aef04730a..169f493ec 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -278,8 +278,8 @@ def products_graphql_query(fields): product_types_var = query.add_variable("productTypes", "[String!]") product_name_regex_var = query.add_variable("productNameRegex", "String!") product_path_regex_var = query.add_variable("productPathRegex", "String!") - statuses_var = query.add_variable("productStatuses.", "[String!]") - tags_var = query.add_variable("productTags.", "[String!]") + statuses_var = query.add_variable("productStatuses", "[String!]") + tags_var = query.add_variable("productTags", "[String!]") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) From a09358d3ecac99acadf669918318afbaf17a4707 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:14:32 +0200 Subject: [PATCH 100/506] implemented actions functionality --- ayon_api/_actions.py | 300 +++++++++++++++++++++++++++++++++++++++++ ayon_api/server_api.py | 3 +- ayon_api/typing.py | 119 ++++++++++++++++ 3 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 ayon_api/_actions.py diff --git a/ayon_api/_actions.py b/ayon_api/_actions.py new file mode 100644 index 000000000..39b554e6f --- /dev/null +++ b/ayon_api/_actions.py @@ -0,0 +1,300 @@ +import typing +from typing import Optional, Dict, List, Any + +from .utils import prepare_query_string +from ._base import _BaseServerAPI + +if typing.TYPE_CHECKING: + from .typing import ( + ActionEntityTypes, + ActionManifestDict, + ActionTriggerResponse, + ActionTakeResponse, + ActionConfigResponse, + ActionModeType, + ) + + +class _ActionsAPI(_BaseServerAPI): + """Implementation of actions API for ServerAPI.""" + def get_actions( + self, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, + ) -> List["ActionManifestDict"]: + """Get actions for a context. + + Args: + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. ('simple', 'dynamic', 'all') + + Returns: + List[ActionManifestDict]: List of action manifests. + + """ + if variant is None: + variant = self.get_default_settings_variant() + query_data = {"variant": variant} + if mode: + query_data["mode"] = mode + query = prepare_query_string(query_data) + kwargs = { + key: value + for key, value in ( + ("projectName", project_name), + ("entityType", entity_type), + ("entityIds", entity_ids), + ("entitySubtypes", entity_subtypes), + ("formData", form_data), + ) + if value is not None + } + response = self.post(f"actions/list{query}", **kwargs) + response.raise_for_status() + return response.data["actions"] + + def trigger_action( + self, + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + ) -> "ActionTriggerResponse": + """Trigger action. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + """ + if variant is None: + variant = self.get_default_settings_variant() + query_data = { + "addonName": addon_name, + "addonVersion": addon_version, + "identifier": identifier, + "variant": variant, + } + query = prepare_query_string(query_data) + + kwargs = { + key: value + for key, value in ( + ("projectName", project_name), + ("entityType", entity_type), + ("entityIds", entity_ids), + ("entitySubtypes", entity_subtypes), + ("formData", form_data), + ) + if value is not None + } + + response = self.post(f"actions/execute{query}", **kwargs) + response.raise_for_status() + return response.data + + def get_action_config( + self, + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: Action configuration data. + + """ + return self._send_config_request( + identifier, + addon_name, + addon_version, + None, + project_name, + entity_type, + entity_ids, + entity_subtypes, + form_data, + variant, + ) + + def set_action_config( + self, + identifier: str, + addon_name: str, + addon_version: str, + value: Dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + ) -> "ActionConfigResponse": + """Set action configuration. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[Dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: New action configuration data. + + """ + return self._send_config_request( + identifier, + addon_name, + addon_version, + value, + project_name, + entity_type, + entity_ids, + entity_subtypes, + form_data, + variant, + ) + + def take_action(self, action_token: str) -> "ActionTakeResponse": + """Take action metadata using an action token. + + Args: + action_token (str): AYON launcher action token. + + Returns: + ActionTakeResponse: Action metadata describing how to launch + action. + + """ + response = self.get(f"actions/abort/{action_token}") + response.raise_for_status() + return response.data + + def abort_action( + self, + action_token: str, + message: Optional[str] = None, + ) -> None: + """Abort action using an action token. + + Args: + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. + + """ + if message is None: + message = "Action aborted" + response = self.post( + f"actions/abort/{action_token}", + message=message, + ) + response.raise_for_status() + + def _send_config_request( + self, + identifier: str, + addon_name: str, + addon_version: str, + value: Optional[Dict[str, Any]], + project_name: Optional[str], + entity_type: Optional["ActionEntityTypes"], + entity_ids: Optional[List[str]], + entity_subtypes: Optional[List[str]], + form_data: Optional[Dict[str, Any]], + variant: Optional[str], + ) -> "ActionConfigResponse": + """Set and get action configuration.""" + if variant is None: + variant = self.get_default_settings_variant() + query_data = { + "addonName": addon_name, + "addonVersion": addon_version, + "identifier": identifier, + "variant": variant, + } + query = prepare_query_string(query_data) + + kwargs = { + query_key: query_value + for query_key, query_value in ( + ("projectName", project_name), + ("entityType", entity_type), + ("entityIds", entity_ids), + ("entitySubtypes", entity_subtypes), + ("formData", form_data), + ) + if query_value is not None + } + if value is not None: + kwargs["value"] = value + + response = self.post(f"actions/config{query}", **kwargs) + response.raise_for_status() + return response.data diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index c1b3e7f97..023b70e95 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -99,6 +99,7 @@ SortOrder, get_machine_name, ) +from ._actions import _ActionsAPI if typing.TYPE_CHECKING: from typing import Union @@ -423,7 +424,7 @@ def as_user(self, username): self._last_user = new_last_user -class ServerAPI(object): +class ServerAPI(_ActionsAPI): """Base handler of connection to server. Requires url to server which is used as base for api and graphql calls. diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 07f041aab..81d854de5 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -10,6 +10,7 @@ BinaryIO, ) + ActivityType = Literal[ "comment", "watch", @@ -35,6 +36,16 @@ ] +IconType = Literal["material-symbols", "url"] + + +class IconDefType(TypedDict): + type: IconType + name: Optional[str] + color: Optional[str] + icon: Optional[str] + + class EventFilterCondition(TypedDict): key: str value: EventFilterValueType @@ -352,4 +363,112 @@ class ProductTypeDict(TypedDict): icon: Optional[str] +ActionEntityTypes = Literal[ + "project", + "folder", + "task", + "product", + "version", + "representation", + "workfile", + "list", +] + + +class ActionManifestDict(TypedDict): + identifier: str + label: str + groupLabel: Optional[str] + category: str + order: int + icon: Optional[IconDefType] + adminOnly: bool + managerOnly: bool + configFields: List[Dict[str, Any]] + featured: bool + addonName: str + addonVersion: str + variant: str + + +ActionResponseType = Literal[ + "form", + "launcher", + "navigate", + "query", + "redirect", + "simple", +] + +ActionModeType = Literal["simple", "dynamic", "all"] + + +class BaseActionPayload(TypedDict): + extra_clipboard: str + extra_download: str + + +class ActionLauncherPayload(BaseActionPayload): + uri: str + + +class ActionNavigatePayload(BaseActionPayload): + uri: str + + +class ActionRedirectPayload(BaseActionPayload): + uri: str + new_tab: bool + + +class ActionQueryPayload(BaseActionPayload): + query: str + + +class ActionFormPayload(BaseActionPayload): + title: str + fields: List[Dict[str, Any]] + submit_label: str + submit_icon: str + cancel_label: str + cancel_icon: str + show_cancel_button: bool + show_submit_button: bool + + +ActionPayload = Union[ + ActionLauncherPayload, + ActionNavigatePayload, + ActionRedirectPayload, + ActionQueryPayload, + ActionFormPayload, +] + +class ActionTriggerResponse(TypedDict): + type: ActionResponseType + success: bool + message: Optional[str] + payload: Optional[ActionPayload] + + +class ActionTakeResponse(TypedDict): + eventId: str + actionIdentifier: str + args: List[str] + context: Dict[str, Any] + addonName: str + addonVersion: str + variant: str + userName: str + + +class ActionConfigResponse(TypedDict): + projectName: str + entityType: str + entitySubtypes: List[str] + entityIds: List[str] + formData: Dict[str, Any] + value: Dict[str, Any] + + StreamType = Union[io.BytesIO, BinaryIO] From 641fed411103a7d1b35abea229d7c5efe0c99d70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:16:01 +0200 Subject: [PATCH 101/506] added public functions --- ayon_api/__init__.py | 12 +++ ayon_api/_api.py | 226 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 373cbad88..cb4f97248 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -243,6 +243,12 @@ get_representation_links, send_batch_operations, send_activities_batch_operations, + get_actions, + trigger_action, + get_action_config, + set_action_config, + take_action, + abort_action, ) @@ -489,4 +495,10 @@ "get_representation_links", "send_batch_operations", "send_activities_batch_operations", + "get_actions", + "trigger_action", + "get_action_config", + "set_action_config", + "take_action", + "abort_action", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 0bfe3ee63..15ea3d9f4 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -65,6 +65,12 @@ FlatFolderDict, ProjectHierarchyDict, ProductTypeDict, + ActionEntityTypes, + ActionManifestDict, + ActionTriggerResponse, + ActionTakeResponse, + ActionConfigResponse, + ActionModeType, StreamType, ) @@ -6726,3 +6732,223 @@ def send_activities_batch_operations( can_fail=can_fail, raise_on_fail=raise_on_fail, ) + + +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> List["ActionManifestDict"]: + """Get actions for a context. + + Args: + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. ('simple', 'dynamic', 'all') + + Returns: + List[ActionManifestDict]: List of action manifests. + + """ + con = get_server_api_connection() + return con.get_actions( + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, + ) + + +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + """ + con = get_server_api_connection() + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) + + +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: Action configuration data. + + """ + con = get_server_api_connection() + return con.get_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) + + +def set_action_config( + identifier: str, + addon_name: str, + addon_version: str, + value: Dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Set action configuration. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[Dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: New action configuration data. + + """ + con = get_server_api_connection() + return con.set_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + value=value, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) + + +def take_action( + action_token: str, +) -> "ActionTakeResponse": + """Take action metadata using an action token. + + Args: + action_token (str): AYON launcher action token. + + Returns: + ActionTakeResponse: Action metadata describing how to launch + action. + + """ + con = get_server_api_connection() + return con.take_action( + action_token=action_token, + ) + + +def abort_action( + action_token: str, + message: Optional[str] = None, +) -> None: + """Abort action using an action token. + + Args: + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. + + """ + con = get_server_api_connection() + return con.abort_action( + action_token=action_token, + message=message, + ) From c1534c4cc55523fb2b9dad12f49f666ccca01311 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:16:33 +0200 Subject: [PATCH 102/506] include '_ActionsAPI' in automated api script --- automated_api.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/automated_api.py b/automated_api.py index 329c6c858..a11dc885a 100644 --- a/automated_api.py +++ b/automated_api.py @@ -30,7 +30,11 @@ sys.modules["unidecode"] = type(sys)("unidecode") import ayon_api # noqa: E402 -from ayon_api.server_api import ServerAPI, _PLACEHOLDER # noqa: E402 +from ayon_api.server_api import ( + ServerAPI, + _PLACEHOLDER, + _ActionsAPI, +) # noqa: E402 from ayon_api.utils import NOT_SET # noqa: E402 EXCLUDED_METHODS = { @@ -285,7 +289,9 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): def prepare_api_functions(api_globals): functions = [] - for attr_name, attr in ServerAPI.__dict__.items(): + _items = list(ServerAPI.__dict__.items()) + _items.extend(_ActionsAPI.__dict__.items()) + for attr_name, attr in _items: if ( attr_name.startswith("_") or attr_name in EXCLUDED_METHODS From 08f4c738d423b6dc30b125daceb4e93e447036d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:17:34 +0200 Subject: [PATCH 103/506] fix kw only argument in automated api script --- automated_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/automated_api.py b/automated_api.py index 329c6c858..74c936f66 100644 --- a/automated_api.py +++ b/automated_api.py @@ -249,6 +249,9 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): body_params.append(f"*{var_positional}") func_params.append(f"*{var_positional}") + elif kw_only: + func_params.append("*") + for param_name, param in kw_only: body_params.append(f"{param_name}={param_name}") func_params.append(_kw_default_to_str(param_name, param, api_globals)) From 54949925f7ed45b20a6c77e037c3c4078e6c538d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:21:30 +0200 Subject: [PATCH 104/506] added missing base class --- ayon_api/_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 ayon_api/_base.py diff --git a/ayon_api/_base.py b/ayon_api/_base.py new file mode 100644 index 000000000..a87d5800b --- /dev/null +++ b/ayon_api/_base.py @@ -0,0 +1,9 @@ +class _BaseServerAPI: + def get_default_settings_variant(self) -> str: + raise NotImplementedError() + + def get(self, entrypoint: str, **kwargs): + pass + + def post(self, entrypoint: str, **kwargs): + pass From 27490c6e56169f0e9708c2e6261a8c79a04e8ded Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:22:10 +0200 Subject: [PATCH 105/506] fix line length --- ayon_api/_actions.py | 2 +- ayon_api/_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_actions.py b/ayon_api/_actions.py index 39b554e6f..a52c711f4 100644 --- a/ayon_api/_actions.py +++ b/ayon_api/_actions.py @@ -41,7 +41,7 @@ def get_actions( folder types for folder ids, task types for tasks ids. form_data (Optional[Dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. ('simple', 'dynamic', 'all') + mode (Optional[ActionModeType]): Action modes. Returns: List[ActionManifestDict]: List of action manifests. diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 15ea3d9f4..bf16f2848 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6757,7 +6757,7 @@ def get_actions( folder types for folder ids, task types for tasks ids. form_data (Optional[Dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. ('simple', 'dynamic', 'all') + mode (Optional[ActionModeType]): Action modes. Returns: List[ActionManifestDict]: List of action manifests. From 2209c1d6c2949656266d6a3f83faad075c0b93a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 1 Aug 2025 18:28:10 +0200 Subject: [PATCH 106/506] move noqa elsewhere --- automated_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/automated_api.py b/automated_api.py index 69998a7b2..da9fc8af0 100644 --- a/automated_api.py +++ b/automated_api.py @@ -30,11 +30,11 @@ sys.modules["unidecode"] = type(sys)("unidecode") import ayon_api # noqa: E402 -from ayon_api.server_api import ( +from ayon_api.server_api import ( # noqa: E402 ServerAPI, _PLACEHOLDER, _ActionsAPI, -) # noqa: E402 +) from ayon_api.utils import NOT_SET # noqa: E402 EXCLUDED_METHODS = { From dc44738191d39849223878bd607e45247e3433ff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:34:39 +0200 Subject: [PATCH 107/506] Fix indentation --- ayon_api/_actions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_actions.py b/ayon_api/_actions.py index a52c711f4..b575189dd 100644 --- a/ayon_api/_actions.py +++ b/ayon_api/_actions.py @@ -137,7 +137,7 @@ def get_action_config( form_data: Optional[Dict[str, Any]] = None, *, variant: Optional[str] = None, -) -> "ActionConfigResponse": + ) -> "ActionConfigResponse": """Get action configuration. Args: From 2bf8570d685f3c92a37e3b08cf6fce7d3a42caf7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 Aug 2025 17:36:46 +0200 Subject: [PATCH 108/506] fix list ids variable type --- 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 7db022e82..54648f7d7 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -670,7 +670,7 @@ def activities_graphql_query(fields, order): def entity_lists_graphql_query(fields): query = GraphQlQuery("EntityLists") project_name_var = query.add_variable("projectName", "String!") - entity_list_ids = query.add_variable("listIds", "String!") + entity_list_ids = query.add_variable("listIds", "[String!]") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) From 13309b55da599161dba40bee0d295a9dd824cbc2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 5 Aug 2025 09:41:02 +0200 Subject: [PATCH 109/506] Fix typos Co-authored-by: Petr Kalis --- ayon_api/_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index cd0bb981c..b2e16fafe 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6844,7 +6844,7 @@ def get_entity_list_attribute_definitions( project_name: str, list_id: str, ) -> List["EntityListAttributeDefinitionDict"]: - """Get attribute definitioins on entity list. + """Get attribute definitions on entity list. Args: project_name (str): Project name. @@ -6867,7 +6867,7 @@ def set_entity_list_attribute_definitions( list_id: str, attribute_definitions: List["EntityListAttributeDefinitionDict"], ) -> None: - """Set attribute definitioins on entity list. + """Set attribute definitions on entity list. Args: project_name (str): Project name. From 4523c8afe1048ef859abf3cadc5bb7c1d17fe91e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:00:25 +0200 Subject: [PATCH 110/506] move lists api to separate class --- automated_api.py | 2 + ayon_api/_base.py | 45 ++++- ayon_api/_lists.py | 414 +++++++++++++++++++++++++++++++++++++++++ ayon_api/server_api.py | 405 +--------------------------------------- 4 files changed, 459 insertions(+), 407 deletions(-) create mode 100644 ayon_api/_lists.py diff --git a/automated_api.py b/automated_api.py index da9fc8af0..16b3ed9cb 100644 --- a/automated_api.py +++ b/automated_api.py @@ -34,6 +34,7 @@ ServerAPI, _PLACEHOLDER, _ActionsAPI, + _ListsAPI, ) from ayon_api.utils import NOT_SET # noqa: E402 @@ -294,6 +295,7 @@ def prepare_api_functions(api_globals): functions = [] _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) + _items.extend(_ListsAPI.__dict__.items()) for attr_name, attr in _items: if ( attr_name.startswith("_") diff --git a/ayon_api/_base.py b/ayon_api/_base.py index a87d5800b..b3863a2c4 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -1,9 +1,46 @@ +import typing +from typing import Set + +if typing.TYPE_CHECKING: + from .typing import AnyEntityDict + + class _BaseServerAPI: + def get(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def post(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def put(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def patch(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def delete(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_get(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_post(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_put(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_patch(self, entrypoint: str, **kwargs): + raise NotImplementedError() + + def raw_delete(self, entrypoint: str, **kwargs): + raise NotImplementedError() + def get_default_settings_variant(self) -> str: raise NotImplementedError() - def get(self, entrypoint: str, **kwargs): - pass + def get_default_fields_for_type(self, entity_type: str) -> Set[str]: + raise NotImplementedError() - def post(self, entrypoint: str, **kwargs): - pass + def _convert_entity_data(self, entity: "AnyEntityDict"): + raise NotImplementedError() diff --git a/ayon_api/_lists.py b/ayon_api/_lists.py new file mode 100644 index 000000000..480c1e613 --- /dev/null +++ b/ayon_api/_lists.py @@ -0,0 +1,414 @@ +import json +import typing +from typing import Optional, Iterable, Any, Dict, List, Generator + +from ._base import _BaseServerAPI +from .utils import create_entity_id +from .graphql_queries import entity_lists_graphql_query + +if typing.TYPE_CHECKING: + from .typing import ( + EntityListEntityType, + EntityListAttributeDefinitionDict, + EntityListItemMode, + ) + + +class _ListsAPI(_BaseServerAPI): + def get_entity_lists( + self, + project_name: str, + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + ) -> Generator[Dict[str, Any], None, None]: + """Fetch entity lists from server. + + Args: + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Generator[Dict[str, Any], None, None]: Entity list entities + matching defined filters. + + """ + if fields is None: + fields = self.get_default_fields_for_type("entityList") + fields = set(fields) + + if active is not None: + fields.add("active") + + filters: Dict[str, Any] = {"projectName": project_name} + if list_ids is not None: + if not list_ids: + return + filters["listIds"] = list(set(list_ids)) + + query = entity_lists_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for entity_list in parsed_data["project"]["entityLists"]: + if active is not None and entity_list["active"] != active: + continue + + attributes = entity_list.get("attributes") + if isinstance(attributes, str): + entity_list["attributes"] = json.loads(attributes) + + self._convert_entity_data(entity_list) + + yield entity_list + + def get_entity_list_rest( + self, project_name: str, list_id: str + ) -> Optional[Dict[str, Any]]: + """Get entity list by id using REST API. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + response = self.get(f"projects/{project_name}/lists/{list_id}") + response.raise_for_status() + return response.data + + def get_entity_list_by_id( + self, + project_name: str, + list_id: str, + fields: Optional[Iterable[str]] = None, + ) -> Optional[Dict[str, Any]]: + """Get entity list by id using GraphQl. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + for entity_list in self.get_entity_lists( + project_name, list_ids=[list_id], active=None, fields=fields + ): + return entity_list + return None + + def create_entity_list( + self, + project_name: str, + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + template: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[List[Dict[str, Any]]] = None, + list_id: Optional[str] = None, + ) -> str: + """Create entity list. + + Args: + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. + + """ + if list_id is None: + list_id = create_entity_id() + kwargs = { + "id": list_id, + "entityType": entity_type, + "label": label, + } + for key, value in ( + ("entityListType", list_type), + ("access", access), + ("attrib", attrib), + ("template", template), + ("tags", tags), + ("owner", owner), + ("data", data), + ("active", active), + ("items", items), + ): + if value is not None: + kwargs[key] = value + + response = self.post( + f"projects/{project_name}/lists/{list_id}/items", + **kwargs + + ) + response.raise_for_status() + return list_id + + def update_entity_list( + self, + project_name: str, + list_id: str, + *, + label: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + ) -> None: + """Update entity list. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + + """ + kwargs = { + key: value + for key, value in ( + ("label", label), + ("access", access), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("owner", owner), + ("active", active), + ) + if value is not None + } + response = self.patch( + f"projects/{project_name}/lists/{list_id}", + **kwargs + ) + response.raise_for_status() + + def delete_entity_list(self, project_name: str, list_id: str) -> None: + """Delete entity list from project. + + Args: + project_name (str): Project name. + list_id (str): Entity list id that will be removed. + + """ + response = self.delete(f"projects/{project_name}/lists/{list_id}") + response.raise_for_status() + + def get_entity_list_attribute_definitions( + self, project_name: str, list_id: str + ) -> List["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + List[EntityListAttributeDefinitionDict]: List of attribute + definitions. + + """ + response = self.get( + f"projects/{project_name}/lists/{list_id}/attributes" + ) + response.raise_for_status() + return response.data + + def set_entity_list_attribute_definitions( + self, + project_name: str, + list_id: str, + attribute_definitions: List["EntityListAttributeDefinitionDict"], + ) -> None: + """Set attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (List[EntityListAttributeDefinitionDict]): + List of attribute definitions. + + """ + response = self.raw_put( + f"projects/{project_name}/lists/{list_id}/attributes", + json=attribute_definitions, + ) + response.raise_for_status() + + def create_entity_list_item( + self, + project_name: str, + list_id: str, + *, + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + item_id: Optional[str] = None, + ) -> str: + """Create entity list item. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. + + Returns: + str: Item id. + + """ + if item_id is None: + item_id = create_entity_id() + kwargs = { + "id": item_id, + "entityId": list_id, + } + for key, value in ( + ("position", position), + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ): + if value is not None: + kwargs[key] = value + + response = self.post( + f"projects/{project_name}/lists/{list_id}/items", + **kwargs + ) + response.raise_for_status() + return item_id + + def update_entity_list_items( + self, + project_name: str, + list_id: str, + items: List[Dict[str, Any]], + mode: "EntityListItemMode", + ) -> None: + """Update items in entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (List[Dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. + + """ + response = self.post( + f"projects/{project_name}/lists/{list_id}/items", + items=items, + mode=mode, + ) + response.raise_for_status() + + def update_entity_list_item( + self, + project_name: str, + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + ) -> None: + """Update item in entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. + + """ + kwargs = {} + for key, value in ( + ("entityId", new_list_id), + ("position", position), + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ): + if value is not None: + kwargs[key] = value + response = self.patch( + f"projects/{project_name}/lists/{list_id}/items/{item_id}", + **kwargs, + ) + response.raise_for_status() + + def delete_entity_list_item( + self, + project_name: str, + list_id: str, + item_id: str, + ) -> None: + """Delete item from entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. + + """ + response = self.delete( + f"projects/{project_name}/lists/{list_id}/items/{item_id}", + ) + response.raise_for_status() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b819a9f9d..4fecb75da 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -72,7 +72,6 @@ events_graphql_query, users_graphql_query, activities_graphql_query, - entity_lists_graphql_query, ) from .exceptions import ( FailedOperations, @@ -102,14 +101,13 @@ get_machine_name, ) from ._actions import _ActionsAPI +from ._lists import _ListsAPI if typing.TYPE_CHECKING: from typing import Union from .typing import ( ActivityType, ActivityReferenceType, - EntityListEntityType, - EntityListItemMode, LinkDirection, EventFilter, AttributeScope, @@ -137,7 +135,6 @@ ProjectHierarchyDict, ProductTypeDict, StreamType, - EntityListAttributeDefinitionDict, ) PatternType = type(re.compile("")) @@ -429,7 +426,7 @@ def as_user(self, username): self._last_user = new_last_user -class ServerAPI(_ActionsAPI): +class ServerAPI(_ListsAPI, _ActionsAPI): """Base handler of connection to server. Requires url to server which is used as base for api and graphql calls. @@ -8882,404 +8879,6 @@ def get_representation_links( project_name, [representation_id], link_types, link_direction )[representation_id] - def get_entity_lists( - self, - project_name: str, - *, - list_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: - """Fetch entity lists from server. - - Args: - project_name (str): Project name where entity lists are. - list_ids (Optional[Iterable[str]]): List of entity list ids to - fetch. - active (Optional[bool]): Filter by active state of entity lists. - fields (Optional[Iterable[str]]): Fields to fetch from server. - - Returns: - Generator[Dict[str, Any], None, None]: Entity list entities - matching defined filters. - - """ - if fields is None: - fields = self.get_default_fields_for_type("entityList") - fields = set(fields) - - if active is not None: - fields.add("active") - - filters = {"projectName": project_name} - if list_ids is not None: - if not list_ids: - return - filters["listIds"] = list(set(list_ids)) - - query = entity_lists_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for entity_list in parsed_data["project"]["entityLists"]: - if active is not None and entity_list["active"] != active: - continue - - attributes = entity_list.get("attributes") - if isinstance(attributes, str): - entity_list["attributes"] = json.loads(attributes) - - self._convert_entity_data(entity_list) - - yield entity_list - - def get_entity_list_rest( - self, project_name: str, list_id: str - ) -> Optional[Dict[str, Any]]: - """Get entity list by id using REST API. - - Args: - project_name (str): Project name. - list_id (str): Entity list id. - - Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. - - """ - response = self.get(f"projects/{project_name}/lists/{list_id}") - response.raise_for_status() - return response.data - - def get_entity_list_by_id( - self, - project_name: str, - list_id: str, - fields: Optional[Iterable[str]] = None, - ) -> Optional[Dict[str, Any]]: - """Get entity list by id using GraphQl. - - Args: - project_name (str): Project name. - list_id (str): Entity list id. - fields (Optional[Iterable[str]]): Fields to fetch from server. - - Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. - - """ - for entity_list in self.get_entity_lists( - project_name, list_ids=[list_id], active=None, fields=fields - ): - return entity_list - return None - - def create_entity_list( - self, - project_name: str, - entity_type: "EntityListEntityType", - label: str, - *, - list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, - list_id: Optional[str] = None, - ) -> str: - """Create entity list. - - Args: - project_name (str): Project name where entity list lives. - entity_type (EntityListEntityType): Which entity types can be - used in list. - label (str): Entity list label. - list_type (Optional[str]): Entity list type. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - template (Optional[dict[str, Any]]): Dynamic list template. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - items (Optional[list[dict[str, Any]]]): Initial items in - entity list. - list_id (Optional[str]): Entity list id. - - """ - if list_id is None: - list_id = create_entity_id() - kwargs = { - "id": list_id, - "entityType": entity_type, - "label": label, - } - for key, value in ( - ("entityListType", list_type), - ("access", access), - ("attrib", attrib), - ("template", template), - ("tags", tags), - ("owner", owner), - ("data", data), - ("active", active), - ("items", items), - ): - if value is not None: - kwargs[key] = value - - response = self.post( - f"projects/{project_name}/lists/{list_id}/items", - **kwargs - - ) - response.raise_for_status() - return list_id - - def update_entity_list( - self, - project_name: str, - list_id: str, - *, - label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, - ) -> None: - """Update entity list. - - Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id that will be updated. - label (Optional[str]): New label of entity list. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - - """ - kwargs = { - key: value - for key, value in ( - ("label", label), - ("access", access), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("owner", owner), - ("active", active), - ) - if value is not None - } - response = self.patch( - f"projects/{project_name}/lists/{list_id}", - **kwargs - ) - response.raise_for_status() - - def delete_entity_list(self, project_name: str, list_id: str) -> None: - """Delete entity list from project. - - Args: - project_name (str): Project name. - list_id (str): Entity list id that will be removed. - - """ - response = self.delete(f"projects/{project_name}/lists/{list_id}") - response.raise_for_status() - - def get_entity_list_attribute_definitions( - self, project_name: str, list_id: str - ) -> List["EntityListAttributeDefinitionDict"]: - """Get attribute definitioins on entity list. - - Args: - project_name (str): Project name. - list_id (str): Entity list id. - - Returns: - List[EntityListAttributeDefinitionDict]: List of attribute - definitions. - - """ - response = self.get( - f"projects/{project_name}/lists/{list_id}/attributes" - ) - response.raise_for_status() - return response.data - - def set_entity_list_attribute_definitions( - self, - project_name: str, - list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], - ) -> None: - """Set attribute definitioins on entity list. - - Args: - project_name (str): Project name. - list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): - List of attribute definitions. - - """ - response = self.raw_put( - f"projects/{project_name}/lists/{list_id}/attributes", - json=attribute_definitions, - ) - response.raise_for_status() - - def create_entity_list_item( - self, - project_name: str, - list_id: str, - *, - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - item_id: Optional[str] = None, - ) -> str: - """Create entity list item. - - Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Item attribute values. - data (Optional[dict[str, Any]]): Item data. - tags (Optional[list[str]]): Tags of item in entity list. - item_id (Optional[str]): Id of item that will be created. - - Returns: - str: Item id. - - """ - if item_id is None: - item_id = create_entity_id() - kwargs = { - "id": item_id, - "entityId": list_id, - } - for key, value in ( - ("position", position), - ("label", label), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ): - if value is not None: - kwargs[key] = value - - response = self.post( - f"projects/{project_name}/lists/{list_id}/items", - **kwargs - ) - response.raise_for_status() - return item_id - - def update_entity_list_items( - self, - project_name: str, - list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", - ) -> None: - """Update items in entity list. - - Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. - mode (EntityListItemMode): Mode of items update. - - """ - response = self.post( - f"projects/{project_name}/lists/{list_id}/items", - items=items, - mode=mode, - ) - response.raise_for_status() - - def update_entity_list_item( - self, - project_name: str, - list_id: str, - item_id: str, - *, - new_list_id: Optional[str], - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - ) -> None: - """Update item in entity list. - - Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id where item lives. - item_id (str): Item id that will be removed from entity list. - new_list_id (Optional[str]): New entity list id where item will be - added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Attributes of item in entity - list. - data (Optional[dict[str, Any]]): Custom data of item in - entity list. - tags (Optional[list[str]]): Tags of item in entity list. - - """ - kwargs = {} - for key, value in ( - ("entityId", new_list_id), - ("position", position), - ("label", label), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ): - if value is not None: - kwargs[key] = value - response = self.patch( - f"projects/{project_name}/lists/{list_id}/items/{item_id}", - **kwargs, - ) - response.raise_for_status() - - def delete_entity_list_item( - self, - project_name: str, - list_id: str, - item_id: str, - ) -> None: - """Delete item from entity list. - - Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id from which item will be removed. - item_id (str): Item id that will be removed from entity list. - - """ - response = self.delete( - f"projects/{project_name}/lists/{list_id}/items/{item_id}", - ) - response.raise_for_status() - # --- Batch operations processing --- def send_batch_operations( self, From cc326e82ee8932414ef84151c13618e67de7a02e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 6 Aug 2025 17:08:31 +0200 Subject: [PATCH 111/506] change order of functions --- ayon_api/__init__.py | 32 +- ayon_api/_api.py | 908 +++++++++++++++++++++---------------------- 2 files changed, 470 insertions(+), 470 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 7515df97f..719c54a1c 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -241,6 +241,14 @@ get_version_links, get_representations_links, get_representation_links, + send_batch_operations, + send_activities_batch_operations, + get_actions, + trigger_action, + get_action_config, + set_action_config, + take_action, + abort_action, get_entity_lists, get_entity_list_rest, get_entity_list_by_id, @@ -253,14 +261,6 @@ update_entity_list_items, update_entity_list_item, delete_entity_list_item, - send_batch_operations, - send_activities_batch_operations, - get_actions, - trigger_action, - get_action_config, - set_action_config, - take_action, - abort_action, ) @@ -505,6 +505,14 @@ "get_version_links", "get_representations_links", "get_representation_links", + "send_batch_operations", + "send_activities_batch_operations", + "get_actions", + "trigger_action", + "get_action_config", + "set_action_config", + "take_action", + "abort_action", "get_entity_lists", "get_entity_list_rest", "get_entity_list_by_id", @@ -517,12 +525,4 @@ "update_entity_list_items", "update_entity_list_item", "delete_entity_list_item", - "send_batch_operations", - "send_activities_batch_operations", - "get_actions", - "trigger_action", - "get_action_config", - "set_action_config", - "take_action", - "abort_action", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 0a7e0d63a..585c6d44d 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6657,662 +6657,662 @@ def get_representation_links( ) -def get_entity_lists( +def send_batch_operations( project_name: str, - *, - list_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: - """Fetch entity lists from server. + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """Post multiple CRUD 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): Project name where entity lists are. - list_ids (Optional[Iterable[str]]): List of entity list ids to - fetch. - active (Optional[bool]): Filter by active state of entity lists. - fields (Optional[Iterable[str]]): Fields to fetch from server. + 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: - Generator[Dict[str, Any], None, None]: Entity list entities - matching defined filters. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_entity_lists( + return con.send_batch_operations( project_name=project_name, - list_ids=list_ids, - active=active, - fields=fields, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_entity_list_rest( +def send_activities_batch_operations( project_name: str, - list_id: str, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using REST API. + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """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): Project name. - list_id (str): Entity list id. + 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: - Optional[Dict[str, Any]]: Entity list data or None if not found. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_entity_list_rest( + return con.send_activities_batch_operations( project_name=project_name, - list_id=list_id, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_entity_list_by_id( - project_name: str, - list_id: str, - fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using GraphQl. +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> List["ActionManifestDict"]: + """Get actions for a context. Args: - project_name (str): Project name. - list_id (str): Entity list id. - fields (Optional[Iterable[str]]): Fields to fetch from server. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + List[ActionManifestDict]: List of action manifests. """ con = get_server_api_connection() - return con.get_entity_list_by_id( + return con.get_actions( project_name=project_name, - list_id=list_id, - fields=fields, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, ) -def create_entity_list( - project_name: str, - entity_type: "EntityListEntityType", - label: str, +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, *, - list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, - list_id: Optional[str] = None, -) -> str: - """Create entity list. + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. Args: - project_name (str): Project name where entity list lives. - entity_type (EntityListEntityType): Which entity types can be - used in list. - label (str): Entity list label. - list_type (Optional[str]): Entity list type. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - template (Optional[dict[str, Any]]): Dynamic list template. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - items (Optional[list[dict[str, Any]]]): Initial items in - entity list. - list_id (Optional[str]): Entity list id. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. """ con = get_server_api_connection() - return con.create_entity_list( + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, entity_type=entity_type, - label=label, - list_type=list_type, - access=access, - attrib=attrib, - data=data, - tags=tags, - template=template, - owner=owner, - active=active, - items=items, - list_id=list_id, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def update_entity_list( - project_name: str, - list_id: str, +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, *, - label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, -) -> None: - """Update entity list. + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id that will be updated. - label (Optional[str]): New label of entity list. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: Action configuration data. """ con = get_server_api_connection() - return con.update_entity_list( + return con.get_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - list_id=list_id, - label=label, - access=access, - attrib=attrib, - data=data, - tags=tags, - owner=owner, - active=active, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def delete_entity_list( - project_name: str, - list_id: str, -) -> None: - """Delete entity list from project. +def set_action_config( + identifier: str, + addon_name: str, + addon_version: str, + value: Dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Set action configuration. Args: - project_name (str): Project name. - list_id (str): Entity list id that will be removed. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[Dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: New action configuration data. """ con = get_server_api_connection() - return con.delete_entity_list( + return con.set_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + value=value, project_name=project_name, - list_id=list_id, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_entity_list_attribute_definitions( - project_name: str, - list_id: str, -) -> List["EntityListAttributeDefinitionDict"]: - """Get attribute definitions on entity list. +def take_action( + action_token: str, +) -> "ActionTakeResponse": + """Take action metadata using an action token. Args: - project_name (str): Project name. - list_id (str): Entity list id. + action_token (str): AYON launcher action token. Returns: - List[EntityListAttributeDefinitionDict]: List of attribute - definitions. + ActionTakeResponse: Action metadata describing how to launch + action. """ con = get_server_api_connection() - return con.get_entity_list_attribute_definitions( - project_name=project_name, - list_id=list_id, + return con.take_action( + action_token=action_token, ) -def set_entity_list_attribute_definitions( - project_name: str, - list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], +def abort_action( + action_token: str, + message: Optional[str] = None, ) -> None: - """Set attribute definitions on entity list. + """Abort action using an action token. Args: - project_name (str): Project name. - list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): - List of attribute definitions. + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. """ con = get_server_api_connection() - return con.set_entity_list_attribute_definitions( - project_name=project_name, - list_id=list_id, - attribute_definitions=attribute_definitions, + return con.abort_action( + action_token=action_token, + message=message, ) -def create_entity_list_item( +def get_entity_lists( project_name: str, - list_id: str, *, - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - item_id: Optional[str] = None, -) -> str: - """Create entity list item. + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, +) -> Generator[Dict[str, Any], None, None]: + """Fetch entity lists from server. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Item attribute values. - data (Optional[dict[str, Any]]): Item data. - tags (Optional[list[str]]): Tags of item in entity list. - item_id (Optional[str]): Id of item that will be created. + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - str: Item id. + Generator[Dict[str, Any], None, None]: Entity list entities + matching defined filters. """ con = get_server_api_connection() - return con.create_entity_list_item( + return con.get_entity_lists( project_name=project_name, - list_id=list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, - item_id=item_id, + list_ids=list_ids, + active=active, + fields=fields, ) -def update_entity_list_items( +def get_entity_list_rest( project_name: str, list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", -) -> None: - """Update items in entity list. +) -> Optional[Dict[str, Any]]: + """Get entity list by id using REST API. Args: - project_name (str): Project name where entity list live. + project_name (str): Project name. list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. - mode (EntityListItemMode): Mode of items update. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.update_entity_list_items( + return con.get_entity_list_rest( project_name=project_name, list_id=list_id, - items=items, - mode=mode, ) -def update_entity_list_item( +def get_entity_list_by_id( project_name: str, list_id: str, - item_id: str, - *, - new_list_id: Optional[str], - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, -) -> None: - """Update item in entity list. + fields: Optional[Iterable[str]] = None, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using GraphQl. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id where item lives. - item_id (str): Item id that will be removed from entity list. - new_list_id (Optional[str]): New entity list id where item will be - added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Attributes of item in entity - list. - data (Optional[dict[str, Any]]): Custom data of item in - entity list. - tags (Optional[list[str]]): Tags of item in entity list. + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.update_entity_list_item( + return con.get_entity_list_by_id( project_name=project_name, list_id=list_id, - item_id=item_id, - new_list_id=new_list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, + fields=fields, ) -def delete_entity_list_item( +def create_entity_list( project_name: str, - list_id: str, - item_id: str, -) -> None: - """Delete item from entity list. + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + template: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[List[Dict[str, Any]]] = None, + list_id: Optional[str] = None, +) -> str: + """Create entity list. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id from which item will be removed. - item_id (str): Item id that will be removed from entity list. + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. """ con = get_server_api_connection() - return con.delete_entity_list_item( + return con.create_entity_list( project_name=project_name, + entity_type=entity_type, + label=label, + list_type=list_type, + access=access, + attrib=attrib, + data=data, + tags=tags, + template=template, + owner=owner, + active=active, + items=items, list_id=list_id, - item_id=item_id, ) -def send_batch_operations( +def update_entity_list( project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD 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. + list_id: str, + *, + label: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, +) -> None: + """Update entity list. 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. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. """ con = get_server_api_connection() - return con.send_batch_operations( + return con.update_entity_list( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + list_id=list_id, + label=label, + access=access, + attrib=attrib, + data=data, + tags=tags, + owner=owner, + active=active, ) -def send_activities_batch_operations( +def delete_entity_list( project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """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. + list_id: str, +) -> None: + """Delete entity list from project. 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. + project_name (str): Project name. + list_id (str): Entity list id that will be removed. """ con = get_server_api_connection() - return con.send_activities_batch_operations( + return con.delete_entity_list( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + list_id=list_id, ) - -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. - - Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. - + +def get_entity_list_attribute_definitions( + project_name: str, + list_id: str, +) -> List["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + Returns: - List[ActionManifestDict]: List of action manifests. + List[EntityListAttributeDefinitionDict]: List of attribute + definitions. """ con = get_server_api_connection() - return con.get_actions( + return con.get_entity_list_attribute_definitions( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - mode=mode, + list_id=list_id, ) -def trigger_action( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. +def set_entity_list_attribute_definitions( + project_name: str, + list_id: str, + attribute_definitions: List["EntityListAttributeDefinitionDict"], +) -> None: + """Set attribute definitioins on entity list. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (List[EntityListAttributeDefinitionDict]): + List of attribute definitions. """ con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.set_entity_list_attribute_definitions( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + list_id=list_id, + attribute_definitions=attribute_definitions, ) -def get_action_config( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, +def create_entity_list_item( + project_name: str, + list_id: str, *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + item_id: Optional[str] = None, +) -> str: + """Create entity list item. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. Returns: - ActionConfigResponse: Action configuration data. + str: Item id. """ con = get_server_api_connection() - return con.get_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.create_entity_list_item( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + list_id=list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + item_id=item_id, ) -def set_action_config( - identifier: str, - addon_name: str, - addon_version: str, - value: Dict[str, Any], - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Set action configuration. +def update_entity_list_items( + project_name: str, + list_id: str, + items: List[Dict[str, Any]], + mode: "EntityListItemMode", +) -> None: + """Update items in entity list. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action - configuration. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - - Returns: - ActionConfigResponse: New action configuration data. + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (List[Dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. """ con = get_server_api_connection() - return con.set_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - value=value, + return con.update_entity_list_items( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + list_id=list_id, + items=items, + mode=mode, ) -def take_action( - action_token: str, -) -> "ActionTakeResponse": - """Take action metadata using an action token. +def update_entity_list_item( + project_name: str, + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, +) -> None: + """Update item in entity list. Args: - action_token (str): AYON launcher action token. - - Returns: - ActionTakeResponse: Action metadata describing how to launch - action. + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. """ con = get_server_api_connection() - return con.take_action( - action_token=action_token, + return con.update_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, + new_list_id=new_list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, ) -def abort_action( - action_token: str, - message: Optional[str] = None, +def delete_entity_list_item( + project_name: str, + list_id: str, + item_id: str, ) -> None: - """Abort action using an action token. + """Delete item from entity list. Args: - action_token (str): AYON launcher action token. - message (Optional[str]): Message to display in the UI. + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. """ con = get_server_api_connection() - return con.abort_action( - action_token=action_token, - message=message, + return con.delete_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, ) From 29fe235d2e20eaa7849d7cd39da35c1f707adc34 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:22:39 +0200 Subject: [PATCH 112/506] bump version to '1.1.4' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 212bc10f6..535d92790 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.4-dev" +__version__ = "1.1.4" diff --git a/pyproject.toml b/pyproject.toml index e69d59c26..cfbd95c89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.4-dev" +version = "1.1.4" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.4-dev" +version = "1.1.4" description = "AYON Python API" authors = [ "ynput.io " From 9911f83558695eb898e6f4500aca472df0a5a1ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 7 Aug 2025 15:23:32 +0200 Subject: [PATCH 113/506] bump version to new dev '1.1.5-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 535d92790..46f78d0a5 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.4" +__version__ = "1.1.5-dev" diff --git a/pyproject.toml b/pyproject.toml index cfbd95c89..f4b9b4098 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.4" +version = "1.1.5-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.4" +version = "1.1.5-dev" description = "AYON Python API" authors = [ "ynput.io " From 5f562ab63164655fc757d470a4e4dcd6bcbf96a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:21:16 +0200 Subject: [PATCH 114/506] added more methods to base class --- ayon_api/_base.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index b3863a2c4..5d7757e0b 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -1,11 +1,21 @@ import typing -from typing import Set +from typing import Set, Optional + +import requests + +from .utils import TransferProgress, RequestType if typing.TYPE_CHECKING: from .typing import AnyEntityDict class _BaseServerAPI: + def get_base_url(self) -> str: + raise NotImplementedError() + + def get_rest_url(self) -> str: + raise NotImplementedError() + def get(self, entrypoint: str, **kwargs): raise NotImplementedError() @@ -42,5 +52,24 @@ def get_default_settings_variant(self) -> str: def get_default_fields_for_type(self, entity_type: str) -> Set[str]: raise NotImplementedError() + def upload_file( + self, + endpoint: str, + filepath: str, + progress: Optional[TransferProgress] = None, + request_type: Optional[RequestType] = None, + **kwargs + ) -> requests.Response: + raise NotImplementedError() + + def download_file( + self, + endpoint: str, + filepath: str, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + raise NotImplementedError() + def _convert_entity_data(self, entity: "AnyEntityDict"): raise NotImplementedError() From 1d5ef253437bee36f8e6ad53ac5b95446cb302f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:21:43 +0200 Subject: [PATCH 115/506] moved request type to utils --- ayon_api/__init__.py | 4 ++-- ayon_api/server_api.py | 18 ++---------------- ayon_api/utils.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 719c54a1c..0baa87a5a 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -1,5 +1,6 @@ from .version import __version__ from .utils import ( + RequestTypes, TransferProgress, slugify_string, create_dependency_package_basename, @@ -12,7 +13,6 @@ SortOrder, ) from .server_api import ( - RequestTypes, ServerAPI, ) @@ -267,6 +267,7 @@ __all__ = ( "__version__", + "RequestTypes", "TransferProgress", "slugify_string", "create_dependency_package_basename", @@ -278,7 +279,6 @@ "abort_web_action_event", "SortOrder", - "RequestTypes", "ServerAPI", "GlobalServerAPI", diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 4fecb75da..361a00a12 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -83,6 +83,8 @@ UnsupportedServerVersion, ) from .utils import ( + RequestType, + RequestTypes, RepresentationParents, RepresentationHierarchy, prepare_query_string, @@ -184,22 +186,6 @@ def _get_description(response): return HTTPStatus(response.status).description -class RequestType: - def __init__(self, name: str): - self.name: str = name - - def __hash__(self): - return self.name.__hash__() - - -class RequestTypes: - get = RequestType("GET") - post = RequestType("POST") - put = RequestType("PUT") - patch = RequestType("PATCH") - delete = RequestType("DELETE") - - class RestApiResponse(object): """API Response.""" diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 6eb1941c7..c075fdf74 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -62,6 +62,22 @@ def parse_value(cls, value, default=None): return default +class RequestType: + def __init__(self, name: str): + self.name: str = name + + def __hash__(self): + return self.name.__hash__() + + +class RequestTypes: + get = RequestType("GET") + post = RequestType("POST") + put = RequestType("PUT") + patch = RequestType("PATCH") + delete = RequestType("DELETE") + + def get_default_timeout() -> float: """Default value for requests timeout. From 8d335974ba60bdcebc7c22c06012cb85b997b947 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:22:14 +0200 Subject: [PATCH 116/506] moved addons logic to separate class --- ayon_api/_addons.py | 217 +++++++++++++++++++++++++++++++++++++++++ ayon_api/server_api.py | 206 ++------------------------------------ 2 files changed, 223 insertions(+), 200 deletions(-) create mode 100644 ayon_api/_addons.py diff --git a/ayon_api/_addons.py b/ayon_api/_addons.py new file mode 100644 index 000000000..10337ec50 --- /dev/null +++ b/ayon_api/_addons.py @@ -0,0 +1,217 @@ +import os +import typing +from typing import Optional + +from .utils import ( + RequestTypes, + prepare_query_string, + TransferProgress, +) +from ._base import _BaseServerAPI + +if typing.TYPE_CHECKING: + from .typing import AddonsInfoDict + + +class _AddonsAPI(_BaseServerAPI): + def get_addon_endpoint( + self, + addon_name: str, + addon_version: str, + *subpaths: str, + ) -> str: + """Calculate endpoint to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + + Returns: + str: Final url. + + """ + ending = "" + if subpaths: + ending = f"/{'/'.join(subpaths)}" + return f"addons/{addon_name}/{addon_version}{ending}" + + def get_addons_info(self, details: bool = True) -> "AddonsInfoDict": + """Get information about addons available on server. + + Args: + details (Optional[bool]): Detailed data with information how + to get client code. + + """ + endpoint = "addons" + if details: + endpoint += "?details=1" + response = self.get(endpoint) + response.raise_for_status() + return response.data + + def get_addon_url( + self, + addon_name: str, + addon_version: str, + *subpaths: str, + use_rest: bool = True, + ) -> str: + """Calculate url to addon route. + + Examples: + + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. + + Returns: + str: Final url. + + """ + endpoint = self.get_addon_endpoint( + addon_name, addon_version, *subpaths + ) + url_base = self.get_base_url() if use_rest else self.get_rest_url() + return f"{url_base}/{endpoint}" + + def delete_addon( + self, + addon_name: str, + purge: Optional[bool] = None, + ) -> None: + """Delete addon from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. + + """ + if purge is not None: + purge = "true" if purge else "false" + query = prepare_query_string({"purge": purge}) + + response = self.delete(f"addons/{addon_name}{query}") + response.raise_for_status() + + def delete_addon_version( + self, + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, + ) -> None: + """Delete addon version from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. + + """ + if purge is not None: + purge = "true" if purge else "false" + query = prepare_query_string({"purge": purge}) + response = self.delete(f"addons/{addon_name}/{addon_version}{query}") + response.raise_for_status() + + def upload_addon_zip( + self, + src_filepath: str, + progress: Optional[TransferProgress] = None, + ): + """Upload addon zip file to server. + + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + + Example output:: + + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + + Args: + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + + Returns: + dict[str, Any]: Response data from server. + + """ + response = self.upload_file( + "addons/install", + src_filepath, + progress=progress, + request_type=RequestTypes.post, + ) + return response.json() + + def download_addon_private_file( + self, + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> str: + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + str: Filepath to downloaded file. + + """ + if not destination_filename: + destination_filename = filename + dst_filepath = os.path.join(destination_dir, destination_filename) + # Filename can contain "subfolders" + dst_dirpath = os.path.dirname(dst_filepath) + os.makedirs(dst_dirpath, exist_ok=True) + + endpoint = self.get_addon_endpoint( + addon_name, + addon_version, + "private", + filename + ) + url = f"{self.get_base_url()}/{endpoint}" + self.download_file( + url, dst_filepath, chunk_size=chunk_size, progress=progress + ) + return dst_filepath diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 361a00a12..b8140a2af 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -103,6 +103,7 @@ get_machine_name, ) from ._actions import _ActionsAPI +from ._addons import _AddonsAPI from ._lists import _ListsAPI if typing.TYPE_CHECKING: @@ -116,7 +117,6 @@ AttributeSchemaDataDict, AttributeSchemaDict, AttributesSchemaDict, - AddonsInfoDict, InstallersInfoDict, DependencyPackagesDict, DevBundleAddonInfoDict, @@ -412,7 +412,11 @@ def as_user(self, username): self._last_user = new_last_user -class ServerAPI(_ListsAPI, _ActionsAPI): +class ServerAPI( + _ListsAPI, + _ActionsAPI, + _AddonsAPI, +): """Base handler of connection to server. Requires url to server which is used as base for api and graphql calls. @@ -2782,133 +2786,6 @@ def get_default_fields_for_type(self, entity_type: str) -> Set[str]: | self.get_attributes_fields_for_type(entity_type) ) - def get_addons_info(self, details: bool = True) -> "AddonsInfoDict": - """Get information about addons available on server. - - Args: - details (Optional[bool]): Detailed data with information how - to get client code. - - """ - endpoint = "addons" - if details: - endpoint += "?details=1" - response = self.get(endpoint) - response.raise_for_status() - return response.data - - def get_addon_endpoint( - self, - addon_name: str, - addon_version: str, - *subpaths: str, - ) -> str: - """Calculate endpoint to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - - Returns: - str: Final url. - - """ - ending = "" - if subpaths: - ending = "/{}".format("/".join(subpaths)) - return f"addons/{addon_name}/{addon_version}{ending}" - - def get_addon_url( - self, - addon_name: str, - addon_version: str, - *subpaths: str, - use_rest: bool = True, - ) -> str: - """Calculate url to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. - - Returns: - str: Final url. - - """ - endpoint = self.get_addon_endpoint( - addon_name, addon_version, *subpaths - ) - url_base = self._base_url if use_rest else self._rest_url - return f"{url_base}/{endpoint}" - - def download_addon_private_file( - self, - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, - ) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. - - """ - if not destination_filename: - destination_filename = filename - dst_filepath = os.path.join(destination_dir, destination_filename) - # Filename can contain "subfolders" - dst_dirpath = os.path.dirname(dst_filepath) - os.makedirs(dst_dirpath, exist_ok=True) - - endpoint = self.get_addon_endpoint( - addon_name, - addon_version, - "private", - filename - ) - url = f"{self._base_url}/{endpoint}" - self.download_file( - url, dst_filepath, chunk_size=chunk_size, progress=progress - ) - return dst_filepath - def get_installers( self, version: Optional[str] = None, @@ -3284,77 +3161,6 @@ def upload_dependency_package( route = self._get_dependency_package_route(dst_filename) self.upload_file(route, src_filepath, progress=progress) - def delete_addon(self, addon_name: str, purge: Optional[bool] = None): - """Delete addon from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. - - """ - if purge is not None: - purge = "true" if purge else "false" - query = prepare_query_string({"purge": purge}) - - response = self.delete(f"addons/{addon_name}{query}") - response.raise_for_status() - - def delete_addon_version( - self, - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, - ): - """Delete addon version from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. - - """ - if purge is not None: - purge = "true" if purge else "false" - query = prepare_query_string({"purge": purge}) - response = self.delete(f"addons/{addon_name}/{addon_version}{query}") - response.raise_for_status() - - def upload_addon_zip( - self, - src_filepath: str, - progress: Optional[TransferProgress] = None, - ): - """Upload addon zip file to server. - - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). - - Example output:: - - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} - - Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - Returns: - dict[str, Any]: Response data from server. - - """ - response = self.upload_file( - "addons/install", - src_filepath, - progress=progress, - request_type=RequestTypes.post, - ) - return response.json() - def get_bundles(self) -> "BundlesInfoDict": """Server bundles with basic information. From b9011f8b39cc6be9cc65f314247ce1b6330cd8ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:22:50 +0200 Subject: [PATCH 117/506] add addons api to automated api --- automated_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/automated_api.py b/automated_api.py index 16b3ed9cb..5b09a1d4e 100644 --- a/automated_api.py +++ b/automated_api.py @@ -34,6 +34,7 @@ ServerAPI, _PLACEHOLDER, _ActionsAPI, + _AddonsAPI, _ListsAPI, ) from ayon_api.utils import NOT_SET # noqa: E402 @@ -295,6 +296,7 @@ def prepare_api_functions(api_globals): functions = [] _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) + _items.extend(_AddonsAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) for attr_name, attr in _items: if ( From ee6ec100f9e6eef208c84fca17a85f649e169c2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 14:41:22 +0200 Subject: [PATCH 118/506] updated order of functions in public api --- ayon_api/__init__.py | 19 ++- ayon_api/_api.py | 394 +++++++++++++++++++++---------------------- 2 files changed, 209 insertions(+), 204 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 0baa87a5a..90fb4374c 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -95,10 +95,6 @@ get_attributes_for_type, get_attributes_fields_for_type, get_default_fields_for_type, - get_addons_info, - get_addon_endpoint, - get_addon_url, - download_addon_private_file, get_installers, create_installer, update_installer, @@ -111,9 +107,6 @@ delete_dependency_package, download_dependency_package, upload_dependency_package, - delete_addon, - delete_addon_version, - upload_addon_zip, get_bundles, create_bundle, update_bundle, @@ -249,6 +242,13 @@ set_action_config, take_action, abort_action, + get_addon_endpoint, + get_addons_info, + get_addon_url, + delete_addon, + delete_addon_version, + upload_addon_zip, + download_addon_private_file, get_entity_lists, get_entity_list_rest, get_entity_list_by_id, @@ -261,6 +261,11 @@ update_entity_list_items, update_entity_list_item, delete_entity_list_item, + get_rest_project, + get_rest_projects, + get_project_names, + get_projects, + get_project, ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 585c6d44d..b6ea440cb 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1758,130 +1758,6 @@ def get_default_fields_for_type( ) -def get_addons_info( - details: bool = True, -) -> "AddonsInfoDict": - """Get information about addons available on server. - - Args: - details (Optional[bool]): Detailed data with information how - to get client code. - - """ - con = get_server_api_connection() - return con.get_addons_info( - details=details, - ) - - -def get_addon_endpoint( - addon_name: str, - addon_version: str, - *subpaths, -) -> str: - """Calculate endpoint to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - - Returns: - str: Final url. - - """ - con = get_server_api_connection() - return con.get_addon_endpoint( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - ) - - -def get_addon_url( - addon_name: str, - addon_version: str, - *subpaths, - use_rest: bool = True, -) -> str: - """Calculate url to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. - - Returns: - str: Final url. - - """ - con = get_server_api_connection() - return con.get_addon_url( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - use_rest=use_rest, - ) - - -def download_addon_private_file( - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. - - """ - con = get_server_api_connection() - return con.download_addon_private_file( - addon_name=addon_name, - addon_version=addon_version, - filename=filename, - destination_dir=destination_dir, - destination_filename=destination_filename, - chunk_size=chunk_size, - progress=progress, - ) - - def get_installers( version: Optional[str] = None, platform_name: Optional[str] = None, @@ -2231,79 +2107,6 @@ def upload_dependency_package( ) -def delete_addon( - addon_name: str, - purge: Optional[bool] = None, -): - """Delete addon from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. - - """ - con = get_server_api_connection() - return con.delete_addon( - addon_name=addon_name, - purge=purge, - ) - - -def delete_addon_version( - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, -): - """Delete addon version from server. - - Delete all versions of addon from server. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. - - """ - con = get_server_api_connection() - return con.delete_addon_version( - addon_name=addon_name, - addon_version=addon_version, - purge=purge, - ) - - -def upload_addon_zip( - src_filepath: str, - progress: Optional[TransferProgress] = None, -): - """Upload addon zip file to server. - - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). - - Example output:: - - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} - - Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - Returns: - dict[str, Any]: Response data from server. - - """ - con = get_server_api_connection() - return con.upload_addon_zip( - src_filepath=src_filepath, - progress=progress, - ) - - def get_bundles() -> "BundlesInfoDict": """Server bundles with basic information. @@ -6957,6 +6760,203 @@ def abort_action( ) +def get_addon_endpoint( + addon_name: str, + addon_version: str, + *subpaths, +) -> str: + """Calculate endpoint to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + + Returns: + str: Final url. + + """ + con = get_server_api_connection() + return con.get_addon_endpoint( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + ) + + +def get_addons_info( + details: bool = True, +) -> "AddonsInfoDict": + """Get information about addons available on server. + + Args: + details (Optional[bool]): Detailed data with information how + to get client code. + + """ + con = get_server_api_connection() + return con.get_addons_info( + details=details, + ) + + +def get_addon_url( + addon_name: str, + addon_version: str, + *subpaths, + use_rest: bool = True, +) -> str: + """Calculate url to addon route. + + Examples: + + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. + + Returns: + str: Final url. + + """ + con = get_server_api_connection() + return con.get_addon_url( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + use_rest=use_rest, + ) + + +def delete_addon( + addon_name: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. + + """ + con = get_server_api_connection() + return con.delete_addon( + addon_name=addon_name, + purge=purge, + ) + + +def delete_addon_version( + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon version from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. + + """ + con = get_server_api_connection() + return con.delete_addon_version( + addon_name=addon_name, + addon_version=addon_version, + purge=purge, + ) + + +def upload_addon_zip( + src_filepath: str, + progress: Optional[TransferProgress] = None, +): + """Upload addon zip file to server. + + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + + Example output:: + + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + + Args: + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + + Returns: + dict[str, Any]: Response data from server. + + """ + con = get_server_api_connection() + return con.upload_addon_zip( + src_filepath=src_filepath, + progress=progress, + ) + + +def download_addon_private_file( + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + str: Filepath to downloaded file. + + """ + con = get_server_api_connection() + return con.download_addon_private_file( + addon_name=addon_name, + addon_version=addon_version, + filename=filename, + destination_dir=destination_dir, + destination_filename=destination_filename, + chunk_size=chunk_size, + progress=progress, + ) + + def get_entity_lists( project_name: str, *, From 4daab3e399b6de520bdb8776b16b0c58bd479e85 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 15:30:54 +0200 Subject: [PATCH 119/506] move 'fill_own_attribs' to utils --- ayon_api/server_api.py | 20 +------------------- ayon_api/utils.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b8140a2af..294ed9a52 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -101,6 +101,7 @@ get_media_mime_type, SortOrder, get_machine_name, + fill_own_attribs, ) from ._actions import _ActionsAPI from ._addons import _AddonsAPI @@ -309,25 +310,6 @@ def __repr__(self): return f"<{self.__class__.__name__}>" -def fill_own_attribs(entity): - if not entity or not entity.get("attrib"): - return - - attributes = entity.get("ownAttrib") - if attributes is None: - return - attributes = set(attributes) - - own_attrib = {} - entity["ownAttrib"] = own_attrib - - for key, value in entity["attrib"].items(): - if key not in attributes: - own_attrib[key] = None - else: - own_attrib[key] = copy.deepcopy(value) - - class _AsUserStack: """Handle stack of users used over server api connection in service mode. diff --git a/ayon_api/utils.py b/ayon_api/utils.py index c075fdf74..1f863233c 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1,6 +1,7 @@ import os import re import datetime +import copy import uuid import string import platform @@ -78,6 +79,39 @@ class RequestTypes: delete = RequestType("DELETE") +def fill_own_attribs(entity: "AnyEntityDict") -> None: + """Fill own attributes. + + Prepare data with own attributes. Prepare data based on a list of + attribute names in 'ownAttrib' and 'attrib'. If is not attribute in + 'ownAttrib' then it's value is set to 'None'. + + This can be used with a project, folder or task entity. All other entities + don't use hierarchical attributes and 'attrib' values are + "real values". + + Args: + entity (dict): Entity dictionary. + + """ + if not entity or not entity.get("attrib"): + return + + attributes = entity.get("ownAttrib") + if attributes is None: + return + attributes = set(attributes) + + own_attrib = {} + entity["ownAttrib"] = own_attrib + + for key, value in entity["attrib"].items(): + if key not in attributes: + own_attrib[key] = None + else: + own_attrib[key] = copy.deepcopy(value) + + def get_default_timeout() -> float: """Default value for requests timeout. From f04edad7c2afda78e6b9b72f3bc756590da9bee6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 16:43:38 +0200 Subject: [PATCH 120/506] added more black magic --- automated_api.py | 142 +++++++++++++++++++++++++++++++---------------- 1 file changed, 94 insertions(+), 48 deletions(-) diff --git a/automated_api.py b/automated_api.py index 5b09a1d4e..c3738bdbc 100644 --- a/automated_api.py +++ b/automated_api.py @@ -20,7 +20,6 @@ import typing # Fake modules to avoid import errors - requests = type(sys)("requests") requests.__dict__["Response"] = type( "Response", (), {"__module__": "requests"} @@ -29,15 +28,7 @@ sys.modules["requests"] = requests sys.modules["unidecode"] = type(sys)("unidecode") -import ayon_api # noqa: E402 -from ayon_api.server_api import ( # noqa: E402 - ServerAPI, - _PLACEHOLDER, - _ActionsAPI, - _AddonsAPI, - _ListsAPI, -) -from ayon_api.utils import NOT_SET # noqa: E402 +CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) EXCLUDED_METHODS = { "get_default_service_username", @@ -126,34 +117,38 @@ def prepare_docstring(func): return f'"""{docstring}{line_char}\n"""' +def _find_obj(obj_full, api_globals): + parts = list(reversed(obj_full.split("."))) + _name = None + for part in parts: + if _name is None: + _name = part + else: + _name = f"{part}.{_name}" + try: + # Test if typehint is valid for known '_api' content + exec(f"_: {_name} = None", api_globals) + return _name + except NameError: + pass + return None + + def _get_typehint(annotation, api_globals): + if isinstance(annotation, str): + annotation = annotation.replace("'", '"') + if inspect.isclass(annotation): - module_name_parts = list(str(annotation.__module__).split(".")) - module_name_parts.append(annotation.__name__) - module_name_parts.reverse() - options = [] - _name = None - for name in module_name_parts: - if _name is None: - _name = name - options.append(name) - else: - _name = f"{name}.{_name}" - options.append(_name) - - options.reverse() - for option in options: - try: - # Test if typehint is valid for known '_api' content - exec(f"_: {option} = None", api_globals) - return option - except NameError: - pass - - typehint = options[0] - print("Unknown typehint:", typehint) - typehint = f'"{typehint}"' - return typehint + module_name = str(annotation.__module__) + full_name = annotation.__name__ + if module_name: + full_name = f"{module_name}.{full_name}" + obj_name = _find_obj(full_name, api_globals) + if obj_name is not None: + return obj_name + + print("Unknown typehint:", full_name) + return f'"{full_name}"' typehint = ( str(annotation) @@ -162,9 +157,15 @@ def _get_typehint(annotation, api_globals): full_path_regex = re.compile( r"(?P(?P[a-zA-Z0-9_\.]+))" ) + for item in full_path_regex.finditer(str(typehint)): groups = item.groupdict() - name = groups["name"].split(".")[-1] + name = groups["name"] + obj_name = _find_obj(name, api_globals) + if obj_name: + name = obj_name + else: + name = name.split(".")[-1] typehint = typehint.replace(groups["full"], name) forwardref_regex = re.compile( @@ -172,16 +173,52 @@ def _get_typehint(annotation, api_globals): ) for item in forwardref_regex.finditer(str(typehint)): groups = item.groupdict() - name = groups["name"].split(".")[-1] - typehint = typehint.replace(groups["full"], f'"{name}"') + name = groups["name"] + obj_name = _find_obj(name, api_globals) + if obj_name: + name = obj_name + else: + name = name.split(".")[-1] + typehint = typehint.replace(groups["full"], name) try: # Test if typehint is valid for known '_api' content exec(f"_: {typehint} = None", api_globals) + return typehint except NameError: print("Unknown typehint:", typehint) - typehint = f'"{typehint}"' - return typehint + + _typehint = typehint + _typehing_parents = [] + while True: + # Too hard to manage typehints with commas + if "[" not in _typehint: + break + + parts = _typehint.split("[") + parent = parts.pop(0) + + try: + # Test if typehint is valid for known '_api' content + exec(f"_: {parent} = None", api_globals) + except NameError: + _typehint = parent + break + + _typehint = "[".join(parts)[:-1] + if "," in _typehint: + _typing = parent + break + + _typehing_parents.append(parent) + + if _typehing_parents: + typehint = f'"{_typehint}"' + for parent in reversed(_typehing_parents): + typehint = f"{parent}[{typehint}]" + return typehint + + return f'"{typehint}"' def _get_param_typehint(param, api_globals): @@ -198,6 +235,9 @@ def _add_typehint(param_name, param, api_globals): def _kw_default_to_str(param_name, param, api_globals): + from ayon_api.server_api import _PLACEHOLDER + from ayon_api.utils import NOT_SET + if param.default is inspect.Parameter.empty: return _add_typehint(param_name, param, api_globals) @@ -293,12 +333,23 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): def prepare_api_functions(api_globals): + from ayon_api.server_api import ( # noqa: E402 + ServerAPI, + _ActionsAPI, + _AddonsAPI, + _ListsAPI, + ) + functions = [] _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) + processed = set() for attr_name, attr in _items: + if attr_name in processed: + continue + processed.add(attr_name) if ( attr_name.startswith("_") or attr_name in EXCLUDED_METHODS @@ -336,10 +387,7 @@ def prepare_api_functions(api_globals): def main(): print("Creating public API functions based on ServerAPI methods") # TODO order methods in some order - dirpath = os.path.dirname(os.path.dirname( - os.path.abspath(ayon_api.__file__) - )) - ayon_api_root = os.path.join(dirpath, "ayon_api") + ayon_api_root = os.path.join(CURRENT_DIR, "ayon_api") init_filepath = os.path.join(ayon_api_root, "__init__.py") api_filepath = os.path.join(ayon_api_root, "_api.py") @@ -363,15 +411,13 @@ def main(): # Read content of first part of `_api.py` to get global variables # - disable type checking so imports done only during typechecking are # not executed - old_value = typing.TYPE_CHECKING typing.TYPE_CHECKING = False api_globals = {"__name__": "ayon_api._api"} exec(parts[0], api_globals) + for attr_name in dir(__builtins__): api_globals[attr_name] = getattr(__builtins__, attr_name) - typing.TYPE_CHECKING = old_value - # print(api_globals) print("(3/5) Preparing functions body based on 'ServerAPI' class") result = prepare_api_functions(api_globals) From 1e9498290258074d3b04fecc9e3708fd49409bbd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:21:56 +0200 Subject: [PATCH 121/506] change imports in init --- ayon_api/__init__.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 90fb4374c..2c5baa8ba 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -364,10 +364,6 @@ "get_attributes_for_type", "get_attributes_fields_for_type", "get_default_fields_for_type", - "get_addons_info", - "get_addon_endpoint", - "get_addon_url", - "download_addon_private_file", "get_installers", "create_installer", "update_installer", @@ -380,9 +376,6 @@ "delete_dependency_package", "download_dependency_package", "upload_dependency_package", - "delete_addon", - "delete_addon_version", - "upload_addon_zip", "get_bundles", "create_bundle", "update_bundle", @@ -518,6 +511,13 @@ "set_action_config", "take_action", "abort_action", + "get_addon_endpoint", + "get_addons_info", + "get_addon_url", + "delete_addon", + "delete_addon_version", + "upload_addon_zip", + "download_addon_private_file", "get_entity_lists", "get_entity_list_rest", "get_entity_list_by_id", From ac96f87834c71d75bbaeb54bd9dce6eb3682e239 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:29:44 +0200 Subject: [PATCH 122/506] separated project methods --- automated_api.py | 3 + ayon_api/__init__.py | 15 +-- ayon_api/_api.py | 258 +++++++++++++++++------------------ ayon_api/_base.py | 8 ++ ayon_api/_projects.py | 297 +++++++++++++++++++++++++++++++++++++++++ ayon_api/server_api.py | 286 +-------------------------------------- 6 files changed, 445 insertions(+), 422 deletions(-) create mode 100644 ayon_api/_projects.py diff --git a/automated_api.py b/automated_api.py index c3738bdbc..d13ee7995 100644 --- a/automated_api.py +++ b/automated_api.py @@ -338,6 +338,7 @@ def prepare_api_functions(api_globals): _ActionsAPI, _AddonsAPI, _ListsAPI, + _ProjectsAPI, ) functions = [] @@ -345,6 +346,8 @@ def prepare_api_functions(api_globals): _items.extend(_ActionsAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) + _items.extend(_ProjectsAPI.__dict__.items()) + processed = set() for attr_name, attr in _items: if attr_name in processed: diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 2c5baa8ba..c9fc2af89 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -137,8 +137,6 @@ get_secret, save_secret, delete_secret, - get_rest_project, - get_rest_projects, get_rest_entity_by_id, get_rest_folder, get_rest_folders, @@ -146,9 +144,6 @@ get_rest_product, get_rest_version, get_rest_representation, - get_project_names, - get_projects, - get_project, get_folders_hierarchy, get_folders_rest, get_folders, @@ -406,8 +401,6 @@ "get_secret", "save_secret", "delete_secret", - "get_rest_project", - "get_rest_projects", "get_rest_entity_by_id", "get_rest_folder", "get_rest_folders", @@ -415,9 +408,6 @@ "get_rest_product", "get_rest_version", "get_rest_representation", - "get_project_names", - "get_projects", - "get_project", "get_folders_hierarchy", "get_folders_rest", "get_folders", @@ -530,4 +520,9 @@ "update_entity_list_items", "update_entity_list_item", "delete_entity_list_item", + "get_rest_project", + "get_rest_projects", + "get_project_names", + "get_projects", + "get_project", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index b6ea440cb..79d776a22 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -3023,52 +3023,6 @@ def delete_secret( ) -def get_rest_project( - project_name: str, -) -> Optional["ProjectDict"]: - """Query project by name. - - This call returns project with anatomy data. - - Args: - project_name (str): Name of project. - - Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. - - """ - con = get_server_api_connection() - return con.get_rest_project( - project_name=project_name, - ) - - -def get_rest_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, -) -> Generator["ProjectDict", None, None]: - """Query available project entities. - - User must be logged in. - - Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - Generator[ProjectDict, None, None]: Available projects. - - """ - con = get_server_api_connection() - return con.get_rest_projects( - active=active, - library=library, - ) - - def get_rest_entity_by_id( project_name: str, entity_type: str, @@ -3200,89 +3154,6 @@ def get_rest_representation( ) -def get_project_names( - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, -) -> List[str]: - """Receive available project names. - - User must be logged in. - - Args: - active (Union[bool, None]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Union[bool, None]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - list[str]: List of available project names. - - """ - con = get_server_api_connection() - return con.get_project_names( - active=active, - library=library, - ) - - -def get_projects( - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["ProjectDict", None, None]: - """Get projects. - - Args: - active (Optional[bool]): Filter active or inactive projects. - Filter is disabled when 'None' is passed. - library (Optional[bool]): Filter library projects. Filter is - disabled when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[ProjectDict, None, None]: Queried projects. - - """ - con = get_server_api_connection() - return con.get_projects( - active=active, - library=library, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_project( - project_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["ProjectDict"]: - """Get project. - - Args: - project_name (str): Name of project. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[ProjectDict]: Project entity data or None - if project was not found. - - """ - con = get_server_api_connection() - return con.get_project( - project_name=project_name, - fields=fields, - own_attributes=own_attributes, - ) - - def get_folders_hierarchy( project_name: str, search_string: Optional[str] = None, @@ -7316,3 +7187,132 @@ def delete_entity_list_item( list_id=list_id, item_id=item_id, ) + + +def get_rest_project( + project_name: str, +) -> Optional["ProjectDict"]: + """Query project by name. + + This call returns project with anatomy data. + + Args: + project_name (str): Name of project. + + Returns: + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. + + """ + con = get_server_api_connection() + return con.get_rest_project( + project_name=project_name, + ) + + +def get_rest_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> Generator["ProjectDict", None, None]: + """Query available project entities. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + Generator[ProjectDict, None, None]: Available projects. + + """ + con = get_server_api_connection() + return con.get_rest_projects( + active=active, + library=library, + ) + + +def get_project_names( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> list[str]: + """Receive available project names. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + list[str]: List of available project names. + + """ + con = get_server_api_connection() + return con.get_project_names( + active=active, + library=library, + ) + + +def get_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Generator["ProjectDict", None, None]: + """Get projects. + + Args: + active (Optional[bool]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Optional[bool]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[ProjectDict, None, None]: Queried projects. + + """ + con = get_server_api_connection() + return con.get_projects( + active=active, + library=library, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_project( + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["ProjectDict"]: + """Get project. + + Args: + project_name (str): Name of project. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[ProjectDict]: Project entity data or None + if project was not found. + + """ + con = get_server_api_connection() + return con.get_project( + project_name=project_name, + fields=fields, + own_attributes=own_attributes, + ) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index 5d7757e0b..4fb92a960 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -71,5 +71,13 @@ def download_file( ) -> TransferProgress: raise NotImplementedError() + def _prepare_fields( + self, + entity_type: str, + fields: set[str], + own_attributes: bool = False, + ): + raise NotImplementedError() + def _convert_entity_data(self, entity: "AnyEntityDict"): raise NotImplementedError() diff --git a/ayon_api/_projects.py b/ayon_api/_projects.py new file mode 100644 index 000000000..62c0523d2 --- /dev/null +++ b/ayon_api/_projects.py @@ -0,0 +1,297 @@ +from __future__ import annotations + +import json +import typing +from typing import Optional, Generator, Iterable, Any + +from ._base import _BaseServerAPI +from .utils import prepare_query_string, fill_own_attribs +from .graphql_queries import projects_graphql_query + +if typing.TYPE_CHECKING: + from .typing import ProjectDict + + +class _ProjectsAPI(_BaseServerAPI): + def get_rest_project( + self, project_name: str + ) -> Optional["ProjectDict"]: + """Query project by name. + + This call returns project with anatomy data. + + Args: + project_name (str): Name of project. + + Returns: + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. + + """ + if not project_name: + return None + + response = self.get(f"projects/{project_name}") + # TODO ignore only error about not existing project + if response.status != 200: + return None + project = response.data + self._fill_project_entity_data(project) + return project + + def get_rest_projects( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + ) -> Generator["ProjectDict", None, None]: + """Query available project entities. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + Generator[ProjectDict, None, None]: Available projects. + + """ + for project_name in self.get_project_names(active, library): + project = self.get_rest_project(project_name) + if project: + yield project + + def get_project_names( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + ) -> list[str]: + """Receive available project names. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + list[str]: List of available project names. + + """ + if active is not None: + active = "true" if active else "false" + + if library is not None: + library = "true" if library else "false" + + query = prepare_query_string({"active": active, "library": library}) + + response = self.get(f"projects{query}") + response.raise_for_status() + data = response.data + project_names = [] + if data: + for project in data["projects"]: + project_names.append(project["name"]) + return project_names + + def get_projects( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Generator["ProjectDict", None, None]: + """Get projects. + + Args: + active (Optional[bool]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Optional[bool]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[ProjectDict, None, None]: Queried projects. + + """ + if fields is not None: + fields = set(fields) + + graphql_fields, use_rest = self._get_project_graphql_fields(fields) + projects_by_name = {} + if graphql_fields: + projects = list(self._get_graphql_projects( + active, + library, + fields=graphql_fields, + own_attributes=own_attributes, + )) + if not use_rest: + yield from projects + return + projects_by_name = {p["name"]: p for p in projects} + + for project in self.get_rest_projects(active, library): + name = project["name"] + graphql_p = projects_by_name.get(name) + if graphql_p: + project["productTypes"] = graphql_p["productTypes"] + yield project + + def get_project( + self, + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional["ProjectDict"]: + """Get project. + + Args: + project_name (str): Name of project. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[ProjectDict]: Project entity data or None + if project was not found. + + """ + if fields is not None: + fields = set(fields) + + graphql_fields, use_rest = self._get_project_graphql_fields(fields) + graphql_project = None + if graphql_fields: + graphql_project = next(self._get_graphql_projects( + None, + None, + fields=graphql_fields, + own_attributes=own_attributes, + ), None) + if not graphql_project or not use_rest: + return graphql_project + + project = self.get_rest_project(project_name) + if own_attributes: + fill_own_attribs(project) + if graphql_project: + project["productTypes"] = graphql_project["productTypes"] + return project + + def _get_project_graphql_fields( + self, fields: Optional[set[str]] + ) -> tuple[set[str], bool]: + """Fetch of project must be done using REST endpoint. + + Returns: + set[str]: GraphQl fields. + + """ + if fields is None: + return set(), True + + has_product_types = False + graphql_fields = set() + for field in fields: + # Product types are available only in GraphQl + if field.startswith("productTypes"): + has_product_types = True + graphql_fields.add(field) + + if not has_product_types: + return set(), True + + inters = fields & {"name", "code", "active", "library"} + remainders = fields - (inters | graphql_fields) + if remainders: + graphql_fields.add("name") + return graphql_fields, True + graphql_fields |= inters + return graphql_fields, False + + def _fill_project_entity_data(self, project: dict[str, Any]) -> None: + # Add fake scope to statuses if not available + if "statuses" in project: + for status in project["statuses"]: + scope = status.get("scope") + if scope is None: + status["scope"] = [ + "folder", + "task", + "product", + "version", + "representation", + "workfile" + ] + + # Convert 'data' from string to dict if needed + if "data" in project: + project_data = project["data"] + if isinstance(project_data, str): + project_data = json.loads(project_data) + project["data"] = project_data + + # Fill 'bundle' from data if is not filled + if "bundle" not in project: + bundle_data = project["data"].get("bundle", {}) + prod_bundle = bundle_data.get("production") + staging_bundle = bundle_data.get("staging") + project["bundle"] = { + "production": prod_bundle, + "staging": staging_bundle, + } + + # Convert 'config' from string to dict if needed + config = project.get("config") + if isinstance(config, str): + project["config"] = json.loads(config) + + # Unifiy 'linkTypes' data structure from REST and GraphQL + if "linkTypes" in project: + for link_type in project["linkTypes"]: + if "data" in link_type: + link_data = link_type.pop("data") + link_type.update(link_data) + if "style" not in link_type: + link_type["style"] = None + if "color" not in link_type: + link_type["color"] = None + + def _get_graphql_projects( + self, + active: Optional[bool], + library: Optional[bool], + fields: set[str], + own_attributes: bool, + project_name: Optional[str] = None + ): + if active is not None: + fields.add("active") + + if library is not None: + fields.add("library") + + self._prepare_fields("project", fields, own_attributes) + + query = projects_graphql_query(fields) + if project_name is not None: + query.set_variable_value("projectName", project_name) + + for parsed_data in query.continuous_query(self): + for project in parsed_data["projects"]: + if active is not None and active is not project["active"]: + continue + if own_attributes: + fill_own_attribs(project) + self._fill_project_entity_data(project) + yield project diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 294ed9a52..a92fbb2d1 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -106,6 +106,7 @@ from ._actions import _ActionsAPI from ._addons import _AddonsAPI from ._lists import _ListsAPI +from ._projects import _ProjectsAPI if typing.TYPE_CHECKING: from typing import Union @@ -395,9 +396,10 @@ def as_user(self, username): class ServerAPI( - _ListsAPI, _ActionsAPI, _AddonsAPI, + _ListsAPI, + _ProjectsAPI, ): """Base handler of connection to server. @@ -4168,55 +4170,6 @@ def delete_secret(self, secret_name: str): return response.data # Entity getters - def get_rest_project( - self, project_name: str - ) -> Optional["ProjectDict"]: - """Query project by name. - - This call returns project with anatomy data. - - Args: - project_name (str): Name of project. - - Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. - - """ - if not project_name: - return None - - response = self.get(f"projects/{project_name}") - # TODO ignore only error about not existing project - if response.status != 200: - return None - project = response.data - self._fill_project_entity_data(project) - return project - - def get_rest_projects( - self, - active: Optional[bool] = True, - library: Optional[bool] = None, - ) -> Generator["ProjectDict", None, None]: - """Query available project entities. - - User must be logged in. - - Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - Generator[ProjectDict, None, None]: Available projects. - - """ - for project_name in self.get_project_names(active, library): - project = self.get_rest_project(project_name) - if project: - yield project def get_rest_entity_by_id( self, @@ -4333,239 +4286,6 @@ def get_rest_representation( project_name, "representation", representation_id ) - def get_project_names( - self, - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, - ) -> List[str]: - """Receive available project names. - - User must be logged in. - - Args: - active (Union[bool, None]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Union[bool, None]): Filter standard/library projects. Both - are returned if 'None' is passed. - - Returns: - list[str]: List of available project names. - - """ - if active is not None: - active = "true" if active else "false" - - if library is not None: - library = "true" if library else "false" - - query = prepare_query_string({"active": active, "library": library}) - - response = self.get(f"projects{query}") - response.raise_for_status() - data = response.data - project_names = [] - if data: - for project in data["projects"]: - project_names.append(project["name"]) - return project_names - - def get_projects( - self, - active: "Union[bool, None]" = True, - library: "Union[bool, None]" = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Generator["ProjectDict", None, None]: - """Get projects. - - Args: - active (Optional[bool]): Filter active or inactive projects. - Filter is disabled when 'None' is passed. - library (Optional[bool]): Filter library projects. Filter is - disabled when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[ProjectDict, None, None]: Queried projects. - - """ - if fields is not None: - fields = set(fields) - - graphql_fields, use_rest = self._get_project_graphql_fields(fields) - projects_by_name = {} - if graphql_fields: - projects = list(self._get_graphql_projects( - active, - library, - fields=graphql_fields, - own_attributes=own_attributes, - )) - if not use_rest: - yield from projects - return - projects_by_name = {p["name"]: p for p in projects} - - for project in self.get_rest_projects(active, library): - name = project["name"] - graphql_p = projects_by_name.get(name) - if graphql_p: - project["productTypes"] = graphql_p["productTypes"] - yield project - - def get_project( - self, - project_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["ProjectDict"]: - """Get project. - - Args: - project_name (str): Name of project. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[ProjectDict]: Project entity data or None - if project was not found. - - """ - if fields is not None: - fields = set(fields) - - graphql_fields, use_rest = self._get_project_graphql_fields(fields) - graphql_project = None - if graphql_fields: - graphql_project = next(self._get_graphql_projects( - None, - None, - fields=graphql_fields, - own_attributes=own_attributes, - ), None) - if not graphql_project or not use_rest: - return graphql_project - - project = self.get_rest_project(project_name) - if own_attributes: - fill_own_attribs(project) - if graphql_project: - project["productTypes"] = graphql_project["productTypes"] - return project - - def _get_project_graphql_fields( - self, fields: Optional[Set[str]] - ) -> Tuple[Set[str], bool]: - """Fetch of project must be done using REST endpoint. - - Returns: - set[str]: GraphQl fields. - - """ - if fields is None: - return set(), True - - has_product_types = False - graphql_fields = set() - for field in fields: - # Product types are available only in GraphQl - if field.startswith("productTypes"): - has_product_types = True - graphql_fields.add(field) - - if not has_product_types: - return set(), True - - inters = fields & {"name", "code", "active", "library"} - remainders = fields - (inters | graphql_fields) - if remainders: - graphql_fields.add("name") - return graphql_fields, True - graphql_fields |= inters - return graphql_fields, False - - def _fill_project_entity_data(self, project: Dict[str, Any]) -> None: - # Add fake scope to statuses if not available - if "statuses" in project: - for status in project["statuses"]: - scope = status.get("scope") - if scope is None: - status["scope"] = [ - "folder", - "task", - "product", - "version", - "representation", - "workfile" - ] - - # Convert 'data' from string to dict if needed - if "data" in project: - project_data = project["data"] - if isinstance(project_data, str): - project_data = json.loads(project_data) - project["data"] = project_data - - # Fill 'bundle' from data if is not filled - if "bundle" not in project: - bundle_data = project["data"].get("bundle", {}) - prod_bundle = bundle_data.get("production") - staging_bundle = bundle_data.get("staging") - project["bundle"] = { - "production": prod_bundle, - "staging": staging_bundle, - } - - # Convert 'config' from string to dict if needed - config = project.get("config") - if isinstance(config, str): - project["config"] = json.loads(config) - - # Unifiy 'linkTypes' data structure from REST and GraphQL - if "linkTypes" in project: - for link_type in project["linkTypes"]: - if "data" in link_type: - link_data = link_type.pop("data") - link_type.update(link_data) - if "style" not in link_type: - link_type["style"] = None - if "color" not in link_type: - link_type["color"] = None - - def _get_graphql_projects( - self, - active: Optional[bool], - library: Optional[bool], - fields: Set[str], - own_attributes: bool, - project_name: Optional[str] = None - ): - if active is not None: - fields.add("active") - - if library is not None: - fields.add("library") - - self._prepare_fields("project", fields, own_attributes) - - query = projects_graphql_query(fields) - if project_name is not None: - query.set_variable_value("projectName", project_name) - - for parsed_data in query.continuous_query(self): - for project in parsed_data["projects"]: - if active is not None and active is not project["active"]: - continue - if own_attributes: - fill_own_attribs(project) - self._fill_project_entity_data(project) - yield project - def get_folders_hierarchy( self, project_name: str, From bbd75dff327f5c302d12e4b52c5056d43eef1fd0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:30:55 +0200 Subject: [PATCH 123/506] added annotations imports --- ayon_api/_base.py | 6 ++++-- ayon_api/server_api.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index 4fb92a960..889617a51 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -1,5 +1,7 @@ +from __future__ import annotations + import typing -from typing import Set, Optional +from typing import Optional import requests @@ -49,7 +51,7 @@ def raw_delete(self, entrypoint: str, **kwargs): def get_default_settings_variant(self) -> str: raise NotImplementedError() - def get_default_fields_for_type(self, entity_type: str) -> Set[str]: + def get_default_fields_for_type(self, entity_type: str) -> set[str]: raise NotImplementedError() def upload_file( diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a92fbb2d1..38f99e32a 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -3,6 +3,8 @@ Provides access to server API. """ +from __future__ import annotations + import os import re import io From ec23b652b04e0906698cbd93690d2dfb53214856 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 17:38:39 +0200 Subject: [PATCH 124/506] move links to separate file --- automated_api.py | 2 + ayon_api/_links.py | 656 +++++++++++++++++++++++++++++++++++++++++ ayon_api/server_api.py | 641 +--------------------------------------- 3 files changed, 660 insertions(+), 639 deletions(-) create mode 100644 ayon_api/_links.py diff --git a/automated_api.py b/automated_api.py index d13ee7995..a5971552e 100644 --- a/automated_api.py +++ b/automated_api.py @@ -337,6 +337,7 @@ def prepare_api_functions(api_globals): ServerAPI, _ActionsAPI, _AddonsAPI, + _LinksAPI, _ListsAPI, _ProjectsAPI, ) @@ -345,6 +346,7 @@ def prepare_api_functions(api_globals): _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) + _items.extend(_LinksAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) _items.extend(_ProjectsAPI.__dict__.items()) diff --git a/ayon_api/_links.py b/ayon_api/_links.py new file mode 100644 index 000000000..4c7a82237 --- /dev/null +++ b/ayon_api/_links.py @@ -0,0 +1,656 @@ +from __future__ import annotations + +import collections +import typing +from typing import Optional, Any, Iterable + +from .graphql_queries import ( + folders_graphql_query, + tasks_graphql_query, + products_graphql_query, + versions_graphql_query, + representations_graphql_query, +) +from ._base import _BaseServerAPI + +if typing.TYPE_CHECKING: + from .typing import LinkDirection + + +class _LinksAPI(_BaseServerAPI): + def get_full_link_type_name( + self, link_type_name: str, input_type: str, output_type: str + ) -> str: + """Calculate full link type name used for query from server. + + Args: + link_type_name (str): Type of link. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Returns: + str: Full name of link type used for query from server. + + """ + return "|".join([link_type_name, input_type, output_type]) + + def get_link_types(self, project_name: str) -> list[dict[str, Any]]: + """All link types available on a project. + + Example output: + [ + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + ] + + Args: + project_name (str): Name of project where to look for link types. + + Returns: + list[dict[str, Any]]: Link types available on project. + + """ + response = self.get(f"projects/{project_name}/links/types") + response.raise_for_status() + return response.data["types"] + + def get_link_type( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + ) -> Optional[dict[str, Any]]: + """Get link type data. + + There is not dedicated REST endpoint to get single link type, + so method 'get_link_types' is used. + + Example output: + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + + Args: + project_name (str): Project where link type is available. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Returns: + Optional[dict[str, Any]]: Link type information. + + """ + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + for link_type in self.get_link_types(project_name): + if link_type["name"] == full_type_name: + return link_type + return None + + def create_link_type( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, + ): + """Create or update link type on server. + + Warning: + Because PUT is used for creation it is also used for update. + + Args: + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Additional data related to link. + + Raises: + HTTPRequestError: Server error happened. + + """ + if data is None: + data = {} + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + response = self.put( + f"projects/{project_name}/links/types/{full_type_name}", + **data + ) + response.raise_for_status() + + def delete_link_type( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + ): + """Remove link type from project. + + Args: + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Raises: + HTTPRequestError: Server error happened. + + """ + full_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type + ) + response = self.delete( + f"projects/{project_name}/links/types/{full_type_name}" + ) + response.raise_for_status() + + def make_sure_link_type_exists( + self, + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, + ): + """Make sure link type exists on a project. + + Args: + project_name (str): Name of project. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Link type related data. + + """ + link_type = self.get_link_type( + project_name, link_type_name, input_type, output_type) + if ( + link_type + and (data is None or data == link_type["data"]) + ): + return + self.create_link_type( + project_name, link_type_name, input_type, output_type, data + ) + + def create_link( + self, + project_name: str, + link_type_name: str, + input_id: str, + input_type: str, + output_id: str, + output_type: str, + link_name: Optional[str] = None, + ): + """Create link between 2 entities. + + Link has a type which must already exists on a project. + + Example output:: + + { + "id": "59a212c0d2e211eda0e20242ac120002" + } + + Args: + project_name (str): Project where the link is created. + link_type_name (str): Type of link. + input_id (str): Input entity id. + input_type (str): Entity type of input entity. + output_id (str): Output entity id. + output_type (str): Entity type of output entity. + link_name (Optional[str]): Name of link. + Available from server version '1.0.0-rc.6'. + + Returns: + dict[str, str]: Information about link. + + Raises: + HTTPRequestError: Server error happened. + + """ + full_link_type_name = self.get_full_link_type_name( + link_type_name, input_type, output_type) + + kwargs = { + "input": input_id, + "output": output_id, + "linkType": full_link_type_name, + } + if link_name: + kwargs["name"] = link_name + + response = self.post( + f"projects/{project_name}/links", **kwargs + ) + response.raise_for_status() + return response.data + + def delete_link(self, project_name: str, link_id: str): + """Remove link by id. + + Args: + project_name (str): Project where link exists. + link_id (str): Id of link. + + Raises: + HTTPRequestError: Server error happened. + + """ + response = self.delete( + f"projects/{project_name}/links/{link_id}" + ) + response.raise_for_status() + + def _prepare_link_filters( + self, + filters: dict[str, Any], + link_types: Optional[Iterable[str], None], + link_direction: Optional["LinkDirection"], + link_names: Optional[Iterable[str]], + link_name_regex: Optional[str], + ) -> bool: + """Add links filters for GraphQl queries. + + Args: + filters (dict[str, Any]): Object where filters will be added. + link_types (Optional[Iterable[str]]): Link types filters. + link_direction (Optional[Literal["in", "out"]]): Direction of + link "in", "out" or 'None' for both. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. + + Returns: + bool: Links are valid, and query from server can happen. + + """ + if link_types is not None: + link_types = set(link_types) + if not link_types: + return False + filters["linkTypes"] = list(link_types) + + if link_names is not None: + link_names = set(link_names) + if not link_names: + return False + filters["linkNames"] = list(link_names) + + if link_direction is not None: + if link_direction not in ("in", "out"): + return False + filters["linkDirection"] = link_direction + + if link_name_regex is not None: + filters["linkNameRegex"] = link_name_regex + return True + + def get_entities_links( + self, + project_name: str, + entity_type: str, + entity_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + link_names: Optional[Iterable[str]] = None, + link_name_regex: Optional[str] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Helper method to get links from server for entity types. + + .. highlight:: text + .. code-block:: text + + Example output: + { + "59a212c0d2e211eda0e20242ac120001": [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ], + ... + } + + Args: + project_name (str): Project where links are. + entity_type (Literal["folder", "task", "product", + "version", "representations"]): Entity type. + entity_ids (Optional[Iterable[str]]): Ids of entities for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by entity ids. + + """ + if entity_type == "folder": + query_func = folders_graphql_query + id_filter_key = "folderIds" + project_sub_key = "folders" + elif entity_type == "task": + query_func = tasks_graphql_query + id_filter_key = "taskIds" + project_sub_key = "tasks" + elif entity_type == "product": + query_func = products_graphql_query + id_filter_key = "productIds" + project_sub_key = "products" + elif entity_type == "version": + query_func = versions_graphql_query + id_filter_key = "versionIds" + project_sub_key = "versions" + elif entity_type == "representation": + query_func = representations_graphql_query + id_filter_key = "representationIds" + project_sub_key = "representations" + else: + raise ValueError("Unknown type \"{}\". Expected {}".format( + entity_type, + ", ".join( + ("folder", "task", "product", "version", "representation") + ) + )) + + output = collections.defaultdict(list) + filters = { + "projectName": project_name + } + if entity_ids is not None: + entity_ids = set(entity_ids) + if not entity_ids: + return output + filters[id_filter_key] = list(entity_ids) + + if not self._prepare_link_filters( + filters, link_types, link_direction, link_names, link_name_regex + ): + return output + + link_fields = {"id", "links"} + query = query_func(link_fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for entity in parsed_data["project"][project_sub_key]: + entity_id = entity["id"] + output[entity_id].extend(entity["links"]) + return output + + def get_folders_links( + self, + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query folders links from server. + + Args: + project_name (str): Project where links are. + folder_ids (Optional[Iterable[str]]): Ids of folders for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by folder ids. + + """ + return self.get_entities_links( + project_name, "folder", folder_ids, link_types, link_direction + ) + + def get_folder_links( + self, + project_name: str, + folder_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> list[dict[str, Any]]: + """Query folder links from server. + + Args: + project_name (str): Project where links are. + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of folder. + + """ + return self.get_folders_links( + project_name, [folder_id], link_types, link_direction + )[folder_id] + + def get_tasks_links( + self, + project_name: str, + task_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query tasks links from server. + + Args: + project_name (str): Project where links are. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by task ids. + + """ + return self.get_entities_links( + project_name, "task", task_ids, link_types, link_direction + ) + + def get_task_links( + self, + project_name: str, + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> list[dict[str, Any]]: + """Query task links from server. + + Args: + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of task. + + """ + return self.get_tasks_links( + project_name, [task_id], link_types, link_direction + )[task_id] + + def get_products_links( + self, + project_name: str, + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. + + Args: + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by product ids. + + """ + return self.get_entities_links( + project_name, "product", product_ids, link_types, link_direction + ) + + def get_product_links( + self, + project_name: str, + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> list[dict[str, Any]]: + """Query product links from server. + + Args: + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of product. + + """ + return self.get_products_links( + project_name, [product_id], link_types, link_direction + )[product_id] + + def get_versions_links( + self, + project_name: str, + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. + + Args: + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by version ids. + + """ + return self.get_entities_links( + project_name, "version", version_ids, link_types, link_direction + ) + + def get_version_links( + self, + project_name: str, + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> list[dict[str, Any]]: + """Query version links from server. + + Args: + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of version. + + """ + return self.get_versions_links( + project_name, [version_id], link_types, link_direction + )[version_id] + + def get_representations_links( + self, + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + ) -> dict[str, list[dict[str, Any]]]: + """Query representations links from server. + + Args: + project_name (str): Project where links are. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by representation ids. + + """ + return self.get_entities_links( + project_name, + "representation", + representation_ids, + link_types, + link_direction + ) + + def get_representation_links( + self, + project_name: str, + representation_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None + ) -> list[dict[str, Any]]: + """Query representation links from server. + + Args: + project_name (str): Project where links are. + representation_id (str): Representation id for which links + should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of representation. + + """ + return self.get_representations_links( + project_name, [representation_id], link_types, link_direction + )[representation_id] \ No newline at end of file diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 38f99e32a..cfa5504bd 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -61,7 +61,6 @@ ) from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( - projects_graphql_query, product_types_query, folders_graphql_query, tasks_graphql_query, @@ -107,6 +106,7 @@ ) from ._actions import _ActionsAPI from ._addons import _AddonsAPI +from ._links import _LinksAPI from ._lists import _ListsAPI from ._projects import _ProjectsAPI @@ -400,6 +400,7 @@ def as_user(self, username): class ServerAPI( _ActionsAPI, _AddonsAPI, + _LinksAPI, _ListsAPI, _ProjectsAPI, ): @@ -7737,644 +7738,6 @@ def delete_project(self, project_name: str): f"Failed to delete project \"{project_name}\". {detail}" ) - # --- Links --- - def get_full_link_type_name( - self, link_type_name: str, input_type: str, output_type: str - ) -> str: - """Calculate full link type name used for query from server. - - Args: - link_type_name (str): Type of link. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Returns: - str: Full name of link type used for query from server. - - """ - return "|".join([link_type_name, input_type, output_type]) - - def get_link_types(self, project_name: str) -> List[Dict[str, Any]]: - """All link types available on a project. - - Example output: - [ - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - ] - - Args: - project_name (str): Name of project where to look for link types. - - Returns: - list[dict[str, Any]]: Link types available on project. - - """ - response = self.get(f"projects/{project_name}/links/types") - response.raise_for_status() - return response.data["types"] - - def get_link_type( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - ) -> Optional[str]: - """Get link type data. - - There is not dedicated REST endpoint to get single link type, - so method 'get_link_types' is used. - - Example output: - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - - Args: - project_name (str): Project where link type is available. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Returns: - Optional[str]: Link type information. - - """ - full_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type - ) - for link_type in self.get_link_types(project_name): - if link_type["name"] == full_type_name: - return link_type - return None - - def create_link_type( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[Dict[str, Any]] = None, - ): - """Create or update link type on server. - - Warning: - Because PUT is used for creation it is also used for update. - - Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Additional data related to link. - - Raises: - HTTPRequestError: Server error happened. - - """ - if data is None: - data = {} - full_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type - ) - response = self.put( - f"projects/{project_name}/links/types/{full_type_name}", - **data - ) - response.raise_for_status() - - def delete_link_type( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - ): - """Remove link type from project. - - Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Raises: - HTTPRequestError: Server error happened. - - """ - full_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type - ) - response = self.delete( - f"projects/{project_name}/links/types/{full_type_name}" - ) - response.raise_for_status() - - def make_sure_link_type_exists( - self, - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[Dict[str, Any]] = None, - ): - """Make sure link type exists on a project. - - Args: - project_name (str): Name of project. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Link type related data. - - """ - link_type = self.get_link_type( - project_name, link_type_name, input_type, output_type) - if ( - link_type - and (data is None or data == link_type["data"]) - ): - return - self.create_link_type( - project_name, link_type_name, input_type, output_type, data - ) - - def create_link( - self, - project_name: str, - link_type_name: str, - input_id: str, - input_type: str, - output_id: str, - output_type: str, - link_name: Optional[str] = None, - ): - """Create link between 2 entities. - - Link has a type which must already exists on a project. - - Example output:: - - { - "id": "59a212c0d2e211eda0e20242ac120002" - } - - Args: - project_name (str): Project where the link is created. - link_type_name (str): Type of link. - input_id (str): Input entity id. - input_type (str): Entity type of input entity. - output_id (str): Output entity id. - output_type (str): Entity type of output entity. - link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. - - Returns: - dict[str, str]: Information about link. - - Raises: - HTTPRequestError: Server error happened. - - """ - full_link_type_name = self.get_full_link_type_name( - link_type_name, input_type, output_type) - - kwargs = { - "input": input_id, - "output": output_id, - "linkType": full_link_type_name, - } - if link_name: - kwargs["name"] = link_name - - response = self.post( - f"projects/{project_name}/links", **kwargs - ) - response.raise_for_status() - return response.data - - def delete_link(self, project_name: str, link_id: str): - """Remove link by id. - - Args: - project_name (str): Project where link exists. - link_id (str): Id of link. - - Raises: - HTTPRequestError: Server error happened. - - """ - response = self.delete( - f"projects/{project_name}/links/{link_id}" - ) - response.raise_for_status() - - def _prepare_link_filters( - self, - filters: Dict[str, Any], - link_types: "Union[Iterable[str], None]", - link_direction: "Union[LinkDirection, None]", - link_names: "Union[Iterable[str], None]", - link_name_regex: "Union[str, None]", - ) -> bool: - """Add links filters for GraphQl queries. - - Args: - filters (dict[str, Any]): Object where filters will be added. - link_types (Union[Iterable[str], None]): Link types filters. - link_direction (Union[Literal["in", "out"], None]): Direction of - link "in", "out" or 'None' for both. - link_names (Union[Iterable[str], None]): Link name filters. - link_name_regex (Union[str, None]): Regex filter for link name. - - Returns: - bool: Links are valid, and query from server can happen. - - """ - if link_types is not None: - link_types = set(link_types) - if not link_types: - return False - filters["linkTypes"] = list(link_types) - - if link_names is not None: - link_names = set(link_names) - if not link_names: - return False - filters["linkNames"] = list(link_names) - - if link_direction is not None: - if link_direction not in ("in", "out"): - return False - filters["linkDirection"] = link_direction - - if link_name_regex is not None: - filters["linkNameRegex"] = link_name_regex - return True - - def get_entities_links( - self, - project_name: str, - entity_type: str, - entity_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - link_names: Optional[Iterable[str]] = None, - link_name_regex: Optional[str] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Helper method to get links from server for entity types. - - .. highlight:: text - .. code-block:: text - - Example output: - { - "59a212c0d2e211eda0e20242ac120001": [ - { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" - }, - ... - ], - ... - } - - Args: - project_name (str): Project where links are. - entity_type (Literal["folder", "task", "product", - "version", "representations"]): Entity type. - entity_ids (Optional[Iterable[str]]): Ids of entities for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - link_names (Optional[Iterable[str]]): Link name filters. - link_name_regex (Optional[str]): Regex filter for link name. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by entity ids. - - """ - if entity_type == "folder": - query_func = folders_graphql_query - id_filter_key = "folderIds" - project_sub_key = "folders" - elif entity_type == "task": - query_func = tasks_graphql_query - id_filter_key = "taskIds" - project_sub_key = "tasks" - elif entity_type == "product": - query_func = products_graphql_query - id_filter_key = "productIds" - project_sub_key = "products" - elif entity_type == "version": - query_func = versions_graphql_query - id_filter_key = "versionIds" - project_sub_key = "versions" - elif entity_type == "representation": - query_func = representations_graphql_query - id_filter_key = "representationIds" - project_sub_key = "representations" - else: - raise ValueError("Unknown type \"{}\". Expected {}".format( - entity_type, - ", ".join( - ("folder", "task", "product", "version", "representation") - ) - )) - - output = collections.defaultdict(list) - filters = { - "projectName": project_name - } - if entity_ids is not None: - entity_ids = set(entity_ids) - if not entity_ids: - return output - filters[id_filter_key] = list(entity_ids) - - if not self._prepare_link_filters( - filters, link_types, link_direction, link_names, link_name_regex - ): - return output - - link_fields = {"id", "links"} - query = query_func(link_fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for entity in parsed_data["project"][project_sub_key]: - entity_id = entity["id"] - output[entity_id].extend(entity["links"]) - return output - - def get_folders_links( - self, - project_name: str, - folder_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query folders links from server. - - Args: - project_name (str): Project where links are. - folder_ids (Optional[Iterable[str]]): Ids of folders for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by folder ids. - - """ - return self.get_entities_links( - project_name, "folder", folder_ids, link_types, link_direction - ) - - def get_folder_links( - self, - project_name: str, - folder_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query folder links from server. - - Args: - project_name (str): Project where links are. - folder_id (str): Folder id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of folder. - - """ - return self.get_folders_links( - project_name, [folder_id], link_types, link_direction - )[folder_id] - - def get_tasks_links( - self, - project_name: str, - task_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query tasks links from server. - - Args: - project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. - - """ - return self.get_entities_links( - project_name, "task", task_ids, link_types, link_direction - ) - - def get_task_links( - self, - project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query task links from server. - - Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of task. - - """ - return self.get_tasks_links( - project_name, [task_id], link_types, link_direction - )[task_id] - - def get_products_links( - self, - project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query products links from server. - - Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. - - """ - return self.get_entities_links( - project_name, "product", product_ids, link_types, link_direction - ) - - def get_product_links( - self, - project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query product links from server. - - Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of product. - - """ - return self.get_products_links( - project_name, [product_id], link_types, link_direction - )[product_id] - - def get_versions_links( - self, - project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query versions links from server. - - Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. - - """ - return self.get_entities_links( - project_name, "version", version_ids, link_types, link_direction - ) - - def get_version_links( - self, - project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> List[Dict[str, Any]]: - """Query version links from server. - - Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of version. - - """ - return self.get_versions_links( - project_name, [version_id], link_types, link_direction - )[version_id] - - def get_representations_links( - self, - project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - ) -> Dict[str, List[Dict[str, Any]]]: - """Query representations links from server. - - Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. - - """ - return self.get_entities_links( - project_name, - "representation", - representation_ids, - link_types, - link_direction - ) - - def get_representation_links( - self, - project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None - ) -> List[Dict[str, Any]]: - """Query representation links from server. - - Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of representation. - - """ - return self.get_representations_links( - project_name, [representation_id], link_types, link_direction - )[representation_id] - # --- Batch operations processing --- def send_batch_operations( self, From 186bc3641e7baf0c25da06b22743daf118a204f8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:05:40 +0200 Subject: [PATCH 125/506] move more methods related to projects --- ayon_api/_projects.py | 149 +++++++++++++++++++++++++++++++++++++++ ayon_api/constants.py | 8 +++ ayon_api/server_api.py | 154 +---------------------------------------- 3 files changed, 158 insertions(+), 153 deletions(-) diff --git a/ayon_api/_projects.py b/ayon_api/_projects.py index 62c0523d2..9edc5948f 100644 --- a/ayon_api/_projects.py +++ b/ayon_api/_projects.py @@ -5,6 +5,7 @@ from typing import Optional, Generator, Iterable, Any from ._base import _BaseServerAPI +from .constants import PROJECT_NAME_REGEX from .utils import prepare_query_string, fill_own_attribs from .graphql_queries import projects_graphql_query @@ -188,6 +189,154 @@ def get_project( project["productTypes"] = graphql_project["productTypes"] return project + def create_project( + self, + project_name: str, + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, + ) -> "ProjectDict": + """Create project using AYON settings. + + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. + + Args: + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. + + Returns: + ProjectDict: Created project entity. + + """ + if self.get_project(project_name): + raise ValueError( + f"Project with name \"{project_name}\" already exists" + ) + + if not PROJECT_NAME_REGEX.match(project_name): + raise ValueError( + f"Project name \"{project_name}\" contain invalid characters" + ) + + preset = self.get_project_anatomy_preset(preset_name) + + result = self.post( + "projects", + name=project_name, + code=project_code, + anatomy=preset, + library=library_project + ) + + if result.status != 201: + details = f"Unknown details ({result.status})" + if result.data: + details = result.data.get("detail") or details + raise ValueError( + f"Failed to create project \"{project_name}\": {details}" + ) + + return self.get_project(project_name) + + def update_project( + self, + project_name: str, + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes + ): + """Update project entity on server. + + Args: + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. + + """ + changes.update({ + key: value + for key, value in ( + ("library", library), + ("folderTypes", folder_types), + ("taskTypes", task_types), + ("linkTypes", link_types), + ("statuses", statuses), + ("tags", tags), + ("config", config), + ("attrib", attrib), + ("data", data), + ("active", active), + ("code", project_code), + ) + if value is not None + }) + response = self.patch( + f"projects/{project_name}", + **changes + ) + response.raise_for_status() + + def delete_project(self, project_name: str): + """Delete project from server. + + This will completely remove project from server without any step back. + + Args: + project_name (str): Project name that will be removed. + + """ + if not self.get_project(project_name): + raise ValueError( + f"Project with name \"{project_name}\" was not found" + ) + + result = self.delete(f"projects/{project_name}") + if result.status_code != 204: + detail = result.data["detail"] + raise ValueError( + f"Failed to delete project \"{project_name}\". {detail}" + ) + def _get_project_graphql_fields( self, fields: Optional[set[str]] ) -> tuple[set[str], bool]: diff --git a/ayon_api/constants.py b/ayon_api/constants.py index d47e4fc59..6dada2de5 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -1,3 +1,5 @@ +import re + # Environments where server url and api key are stored for global connection SERVER_URL_ENV_KEY = "AYON_SERVER_URL" SERVER_API_ENV_KEY = "AYON_API_KEY" @@ -11,6 +13,12 @@ # Backwards compatibility SERVER_TOKEN_ENV_KEY = SERVER_API_ENV_KEY +# This should be collected from server schema +PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" +PROJECT_NAME_REGEX = re.compile( + f"^[{PROJECT_NAME_ALLOWED_SYMBOLS}]+$" +) + # --- User --- DEFAULT_USER_FIELDS = { "accessGroups", diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index cfa5504bd..80dc669aa 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -145,11 +145,7 @@ PatternType = type(re.compile("")) JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) -# This should be collected from server schema -PROJECT_NAME_ALLOWED_SYMBOLS = "a-zA-Z0-9_" -PROJECT_NAME_REGEX = re.compile( - "^[{}]+$".format(PROJECT_NAME_ALLOWED_SYMBOLS) -) + _PLACEHOLDER = object() VERSION_REGEX = re.compile( @@ -7590,154 +7586,6 @@ def update_thumbnail( ) response.raise_for_status() - def create_project( - self, - project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, - ) -> "ProjectDict": - """Create project using AYON settings. - - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. - - Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. - - Raises: - ValueError: When project name already exists. - - Returns: - ProjectDict: Created project entity. - - """ - if self.get_project(project_name): - raise ValueError( - f"Project with name \"{project_name}\" already exists" - ) - - if not PROJECT_NAME_REGEX.match(project_name): - raise ValueError( - f"Project name \"{project_name}\" contain invalid characters" - ) - - preset = self.get_project_anatomy_preset(preset_name) - - result = self.post( - "projects", - name=project_name, - code=project_code, - anatomy=preset, - library=library_project - ) - - if result.status != 201: - details = f"Unknown details ({result.status})" - if result.data: - details = result.data.get("detail") or details - raise ValueError( - f"Failed to create project \"{project_name}\": {details}" - ) - - return self.get_project(project_name) - - def update_project( - self, - project_name: str, - library: Optional[bool] = None, - folder_types: Optional[List[Dict[str, Any]]] = None, - task_types: Optional[List[Dict[str, Any]]] = None, - link_types: Optional[List[Dict[str, Any]]] = None, - statuses: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[Dict[str, Any]]] = None, - config: Optional[Dict[str, Any]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes - ): - """Update project entity on server. - - Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. - - """ - changes.update({ - key: value - for key, value in ( - ("library", library), - ("folderTypes", folder_types), - ("taskTypes", task_types), - ("linkTypes", link_types), - ("statuses", statuses), - ("tags", tags), - ("config", config), - ("attrib", attrib), - ("data", data), - ("active", active), - ("code", project_code), - ) - if value is not None - }) - response = self.patch( - f"projects/{project_name}", - **changes - ) - response.raise_for_status() - - def delete_project(self, project_name: str): - """Delete project from server. - - This will completely remove project from server without any step back. - - Args: - project_name (str): Project name that will be removed. - - """ - if not self.get_project(project_name): - raise ValueError( - f"Project with name \"{project_name}\" was not found" - ) - - result = self.delete(f"projects/{project_name}") - if result.status_code != 204: - detail = result.data["detail"] - raise ValueError( - f"Failed to delete project \"{project_name}\". {detail}" - ) - # --- Batch operations processing --- def send_batch_operations( self, From 4841222568e449a6675a999d7b3f1a0625f8e715 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:09:45 +0200 Subject: [PATCH 126/506] change order of api functions --- ayon_api/__init__.py | 12 +-- ayon_api/_api.py | 232 +++++++++++++++++++++---------------------- 2 files changed, 122 insertions(+), 122 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index c9fc2af89..abdb78bd6 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -207,9 +207,6 @@ get_workfile_thumbnail, create_thumbnail, update_thumbnail, - create_project, - update_project, - delete_project, get_full_link_type_name, get_link_types, get_link_type, @@ -261,6 +258,9 @@ get_project_names, get_projects, get_project, + create_project, + update_project, + delete_project, ) @@ -471,9 +471,6 @@ "get_workfile_thumbnail", "create_thumbnail", "update_thumbnail", - "create_project", - "update_project", - "delete_project", "get_full_link_type_name", "get_link_types", "get_link_type", @@ -525,4 +522,7 @@ "get_project_names", "get_projects", "get_project", + "create_project", + "update_project", + "delete_project", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 79d776a22..edd1bd18c 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5621,122 +5621,6 @@ def update_thumbnail( ) -def create_project( - project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, -) -> "ProjectDict": - """Create project using AYON settings. - - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. - - Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. - - Raises: - ValueError: When project name already exists. - - Returns: - ProjectDict: Created project entity. - - """ - con = get_server_api_connection() - return con.create_project( - project_name=project_name, - project_code=project_code, - library_project=library_project, - preset_name=preset_name, - ) - - -def update_project( - project_name: str, - library: Optional[bool] = None, - folder_types: Optional[List[Dict[str, Any]]] = None, - task_types: Optional[List[Dict[str, Any]]] = None, - link_types: Optional[List[Dict[str, Any]]] = None, - statuses: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[Dict[str, Any]]] = None, - config: Optional[Dict[str, Any]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes, -): - """Update project entity on server. - - Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. - - """ - con = get_server_api_connection() - return con.update_project( - project_name=project_name, - library=library, - folder_types=folder_types, - task_types=task_types, - link_types=link_types, - statuses=statuses, - tags=tags, - config=config, - attrib=attrib, - data=data, - active=active, - project_code=project_code, - **changes, - ) - - -def delete_project( - project_name: str, -): - """Delete project from server. - - This will completely remove project from server without any step back. - - Args: - project_name (str): Project name that will be removed. - - """ - con = get_server_api_connection() - return con.delete_project( - project_name=project_name, - ) - - def get_full_link_type_name( link_type_name: str, input_type: str, @@ -7316,3 +7200,119 @@ def get_project( fields=fields, own_attributes=own_attributes, ) + + +def create_project( + project_name: str, + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, +) -> "ProjectDict": + """Create project using AYON settings. + + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. + + Args: + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. + + Returns: + ProjectDict: Created project entity. + + """ + con = get_server_api_connection() + return con.create_project( + project_name=project_name, + project_code=project_code, + library_project=library_project, + preset_name=preset_name, + ) + + +def update_project( + project_name: str, + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes, +): + """Update project entity on server. + + Args: + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. + + """ + con = get_server_api_connection() + return con.update_project( + project_name=project_name, + library=library, + folder_types=folder_types, + task_types=task_types, + link_types=link_types, + statuses=statuses, + tags=tags, + config=config, + attrib=attrib, + data=data, + active=active, + project_code=project_code, + **changes, + ) + + +def delete_project( + project_name: str, +): + """Delete project from server. + + This will completely remove project from server without any step back. + + Args: + project_name (str): Project name that will be removed. + + """ + con = get_server_api_connection() + return con.delete_project( + project_name=project_name, + ) From f1114ae860df7ef498f9c60d0736a660d99aa48b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:10:17 +0200 Subject: [PATCH 127/506] change links order --- ayon_api/__init__.py | 60 +- ayon_api/_api.py | 1520 +++++++++++++++++++++--------------------- 2 files changed, 790 insertions(+), 790 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index abdb78bd6..a7c41c734 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -207,6 +207,21 @@ get_workfile_thumbnail, create_thumbnail, update_thumbnail, + send_batch_operations, + send_activities_batch_operations, + get_actions, + trigger_action, + get_action_config, + set_action_config, + take_action, + abort_action, + get_addon_endpoint, + get_addons_info, + get_addon_url, + delete_addon, + delete_addon_version, + upload_addon_zip, + download_addon_private_file, get_full_link_type_name, get_link_types, get_link_type, @@ -226,21 +241,6 @@ get_version_links, get_representations_links, get_representation_links, - send_batch_operations, - send_activities_batch_operations, - get_actions, - trigger_action, - get_action_config, - set_action_config, - take_action, - abort_action, - get_addon_endpoint, - get_addons_info, - get_addon_url, - delete_addon, - delete_addon_version, - upload_addon_zip, - download_addon_private_file, get_entity_lists, get_entity_list_rest, get_entity_list_by_id, @@ -471,6 +471,21 @@ "get_workfile_thumbnail", "create_thumbnail", "update_thumbnail", + "send_batch_operations", + "send_activities_batch_operations", + "get_actions", + "trigger_action", + "get_action_config", + "set_action_config", + "take_action", + "abort_action", + "get_addon_endpoint", + "get_addons_info", + "get_addon_url", + "delete_addon", + "delete_addon_version", + "upload_addon_zip", + "download_addon_private_file", "get_full_link_type_name", "get_link_types", "get_link_type", @@ -490,21 +505,6 @@ "get_version_links", "get_representations_links", "get_representation_links", - "send_batch_operations", - "send_activities_batch_operations", - "get_actions", - "trigger_action", - "get_action_config", - "set_action_config", - "take_action", - "abort_action", - "get_addon_endpoint", - "get_addons_info", - "get_addon_url", - "delete_addon", - "delete_addon_version", - "upload_addon_zip", - "download_addon_private_file", "get_entity_lists", "get_entity_list_rest", "get_entity_list_by_id", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index edd1bd18c..26105e319 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5621,1094 +5621,1094 @@ def update_thumbnail( ) -def get_full_link_type_name( - link_type_name: str, - input_type: str, - output_type: str, -) -> str: - """Calculate full link type name used for query from server. +def send_batch_operations( + project_name: str, + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """Post multiple CRUD 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: - link_type_name (str): Type of link. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + 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: - str: Full name of link type used for query from server. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_full_link_type_name( - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + return con.send_batch_operations( + project_name=project_name, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_link_types( +def send_activities_batch_operations( project_name: str, + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, ) -> List[Dict[str, Any]]: - """All link types available on a project. + """Post multiple CRUD activities operations to server. - Example output: - [ - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - ] + 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): Name of project where to look for link types. + 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]]: Link types available on project. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_link_types( + return con.send_activities_batch_operations( project_name=project_name, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_link_type( - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -) -> Optional[str]: - """Get link type data. - - There is not dedicated REST endpoint to get single link type, - so method 'get_link_types' is used. - - Example output: - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> List["ActionManifestDict"]: + """Get actions for a context. Args: - project_name (str): Project where link type is available. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. Returns: - Optional[str]: Link type information. + List[ActionManifestDict]: List of action manifests. """ con = get_server_api_connection() - return con.get_link_type( + return con.get_actions( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, ) -def create_link_type( - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[Dict[str, Any]] = None, -): - """Create or update link type on server. - - Warning: - Because PUT is used for creation it is also used for update. +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Additional data related to link. - - Raises: - HTTPRequestError: Server error happened. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. """ con = get_server_api_connection() - return con.create_link_type( + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def delete_link_type( - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -): - """Remove link type from project. +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. - Raises: - HTTPRequestError: Server error happened. + Returns: + ActionConfigResponse: Action configuration data. """ con = get_server_api_connection() - return con.delete_link_type( + return con.get_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def make_sure_link_type_exists( - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[Dict[str, Any]] = None, -): - """Make sure link type exists on a project. +def set_action_config( + identifier: str, + addon_name: str, + addon_version: str, + value: Dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Set action configuration. Args: - project_name (str): Name of project. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Link type related data. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[Dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: New action configuration data. """ con = get_server_api_connection() - return con.make_sure_link_type_exists( + return con.set_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + value=value, project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def create_link( - project_name: str, - link_type_name: str, - input_id: str, - input_type: str, - output_id: str, - output_type: str, - link_name: Optional[str] = None, -): - """Create link between 2 entities. - - Link has a type which must already exists on a project. - - Example output:: - - { - "id": "59a212c0d2e211eda0e20242ac120002" - } +def take_action( + action_token: str, +) -> "ActionTakeResponse": + """Take action metadata using an action token. Args: - project_name (str): Project where the link is created. - link_type_name (str): Type of link. - input_id (str): Input entity id. - input_type (str): Entity type of input entity. - output_id (str): Output entity id. - output_type (str): Entity type of output entity. - link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + action_token (str): AYON launcher action token. Returns: - dict[str, str]: Information about link. - - Raises: - HTTPRequestError: Server error happened. + ActionTakeResponse: Action metadata describing how to launch + action. """ con = get_server_api_connection() - return con.create_link( - project_name=project_name, - link_type_name=link_type_name, - input_id=input_id, - input_type=input_type, - output_id=output_id, - output_type=output_type, - link_name=link_name, + return con.take_action( + action_token=action_token, ) -def delete_link( - project_name: str, - link_id: str, -): - """Remove link by id. +def abort_action( + action_token: str, + message: Optional[str] = None, +) -> None: + """Abort action using an action token. Args: - project_name (str): Project where link exists. - link_id (str): Id of link. - - Raises: - HTTPRequestError: Server error happened. + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. """ con = get_server_api_connection() - return con.delete_link( - project_name=project_name, - link_id=link_id, + return con.abort_action( + action_token=action_token, + message=message, ) -def get_entities_links( - project_name: str, - entity_type: str, - entity_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - link_names: Optional[Iterable[str]] = None, - link_name_regex: Optional[str] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Helper method to get links from server for entity types. - - .. highlight:: text - .. code-block:: text +def get_addon_endpoint( + addon_name: str, + addon_version: str, + *subpaths, +) -> str: + """Calculate endpoint to addon route. - Example output: - { - "59a212c0d2e211eda0e20242ac120001": [ - { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" - }, - ... - ], - ... - } + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' Args: - project_name (str): Project where links are. - entity_type (Literal["folder", "task", "product", - "version", "representations"]): Entity type. - entity_ids (Optional[Iterable[str]]): Ids of entities for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - link_names (Optional[Iterable[str]]): Link name filters. - link_name_regex (Optional[str]): Regex filter for link name. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. Returns: - dict[str, list[dict[str, Any]]]: Link info by entity ids. + str: Final url. """ con = get_server_api_connection() - return con.get_entities_links( - project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - link_types=link_types, - link_direction=link_direction, - link_names=link_names, - link_name_regex=link_name_regex, + return con.get_addon_endpoint( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, ) -def get_folders_links( - project_name: str, - folder_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query folders links from server. +def get_addons_info( + details: bool = True, +) -> "AddonsInfoDict": + """Get information about addons available on server. Args: - project_name (str): Project where links are. - folder_ids (Optional[Iterable[str]]): Ids of folders for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by folder ids. + details (Optional[bool]): Detailed data with information how + to get client code. """ con = get_server_api_connection() - return con.get_folders_links( - project_name=project_name, - folder_ids=folder_ids, - link_types=link_types, - link_direction=link_direction, + return con.get_addons_info( + details=details, ) -def get_folder_links( - project_name: str, - folder_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query folder links from server. +def get_addon_url( + addon_name: str, + addon_version: str, + *subpaths, + use_rest: bool = True, +) -> str: + """Calculate url to addon route. + + Examples: + + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' Args: - project_name (str): Project where links are. - folder_id (str): Folder id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. Returns: - list[dict[str, Any]]: Link info of folder. + str: Final url. """ con = get_server_api_connection() - return con.get_folder_links( - project_name=project_name, - folder_id=folder_id, - link_types=link_types, - link_direction=link_direction, + return con.get_addon_url( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + use_rest=use_rest, ) -def get_tasks_links( - project_name: str, - task_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query tasks links from server. +def delete_addon( + addon_name: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon from server. - Args: - project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + Delete all versions of addon from server. - Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. + Args: + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_tasks_links( - project_name=project_name, - task_ids=task_ids, - link_types=link_types, - link_direction=link_direction, + return con.delete_addon( + addon_name=addon_name, + purge=purge, ) -def get_task_links( - project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query task links from server. +def delete_addon_version( + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon version from server. - Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + Delete all versions of addon from server. - Returns: - list[dict[str, Any]]: Link info of task. + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_task_links( - project_name=project_name, - task_id=task_id, - link_types=link_types, - link_direction=link_direction, + return con.delete_addon_version( + addon_name=addon_name, + addon_version=addon_version, + purge=purge, ) -def get_products_links( - project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query products links from server. - - Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. +def upload_addon_zip( + src_filepath: str, + progress: Optional[TransferProgress] = None, +): + """Upload addon zip file to server. - """ - con = get_server_api_connection() - return con.get_products_links( - project_name=project_name, - product_ids=product_ids, - link_types=link_types, - link_direction=link_direction, - ) + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + Example output:: -def get_product_links( - project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query product links from server. + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. Returns: - list[dict[str, Any]]: Link info of product. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.get_product_links( - project_name=project_name, - product_id=product_id, - link_types=link_types, - link_direction=link_direction, + return con.upload_addon_zip( + src_filepath=src_filepath, + progress=progress, ) -def get_versions_links( - project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query versions links from server. +def download_addon_private_file( + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. + str: Filepath to downloaded file. """ con = get_server_api_connection() - return con.get_versions_links( - project_name=project_name, - version_ids=version_ids, - link_types=link_types, - link_direction=link_direction, + return con.download_addon_private_file( + addon_name=addon_name, + addon_version=addon_version, + filename=filename, + destination_dir=destination_dir, + destination_filename=destination_filename, + chunk_size=chunk_size, + progress=progress, ) -def get_version_links( - project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query version links from server. +def get_full_link_type_name( + link_type_name: str, + input_type: str, + output_type: str, +) -> str: + """Calculate full link type name used for query from server. Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + link_type_name (str): Type of link. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. Returns: - list[dict[str, Any]]: Link info of version. + str: Full name of link type used for query from server. - """ - con = get_server_api_connection() - return con.get_version_links( - project_name=project_name, - version_id=version_id, - link_types=link_types, - link_direction=link_direction, + """ + con = get_server_api_connection() + return con.get_full_link_type_name( + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_representations_links( +def get_link_types( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> Dict[str, List[Dict[str, Any]]]: - """Query representations links from server. +) -> list[dict[str, Any]]: + """All link types available on a project. + + Example output: + [ + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + ] Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where to look for link types. Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. + list[dict[str, Any]]: Link types available on project. """ con = get_server_api_connection() - return con.get_representations_links( + return con.get_link_types( project_name=project_name, - representation_ids=representation_ids, - link_types=link_types, - link_direction=link_direction, ) -def get_representation_links( +def get_link_type( project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> List[Dict[str, Any]]: - """Query representation links from server. + link_type_name: str, + input_type: str, + output_type: str, +) -> Optional[dict[str, Any]]: + """Get link type data. + + There is not dedicated REST endpoint to get single link type, + so method 'get_link_types' is used. + + Example output: + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project where link type is available. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. Returns: - list[dict[str, Any]]: Link info of representation. + Optional[dict[str, Any]]: Link type information. """ con = get_server_api_connection() - return con.get_representation_links( + return con.get_link_type( project_name=project_name, - representation_id=representation_id, - link_types=link_types, - link_direction=link_direction, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def send_batch_operations( +def create_link_type( project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD operations to server. + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, +): + """Create or update link type on 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. + Warning: + Because PUT is used for creation it is also used for update. 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'. + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Additional data related to link. 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. + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.send_batch_operations( + return con.create_link_type( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def send_activities_batch_operations( +def delete_link_type( project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """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. + link_type_name: str, + input_type: str, + output_type: str, +): + """Remove link type from project. 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'. + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. 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. + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.send_activities_batch_operations( + return con.delete_link_type( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. +def make_sure_link_type_exists( + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, +): + """Make sure link type exists on a project. Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. - - Returns: - List[ActionManifestDict]: List of action manifests. + project_name (str): Name of project. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Link type related data. """ con = get_server_api_connection() - return con.get_actions( + return con.make_sure_link_type_exists( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - mode=mode, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def trigger_action( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. +def create_link( + project_name: str, + link_type_name: str, + input_id: str, + input_type: str, + output_id: str, + output_type: str, + link_name: Optional[str] = None, +): + """Create link between 2 entities. + + Link has a type which must already exists on a project. + + Example output:: + + { + "id": "59a212c0d2e211eda0e20242ac120002" + } + + Args: + project_name (str): Project where the link is created. + link_type_name (str): Type of link. + input_id (str): Input entity id. + input_type (str): Entity type of input entity. + output_id (str): Output entity id. + output_type (str): Entity type of output entity. + link_name (Optional[str]): Name of link. + Available from server version '1.0.0-rc.6'. - Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + Returns: + dict[str, str]: Information about link. + + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.create_link( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + link_type_name=link_type_name, + input_id=input_id, + input_type=input_type, + output_id=output_id, + output_type=output_type, + link_name=link_name, ) -def get_action_config( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. +def delete_link( + project_name: str, + link_id: str, +): + """Remove link by id. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where link exists. + link_id (str): Id of link. - Returns: - ActionConfigResponse: Action configuration data. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.delete_link( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + link_id=link_id, ) -def set_action_config( - identifier: str, - addon_name: str, - addon_version: str, - value: Dict[str, Any], - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Set action configuration. +def get_entities_links( + project_name: str, + entity_type: str, + entity_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + link_names: Optional[Iterable[str]] = None, + link_name_regex: Optional[str] = None, +) -> dict[str, list[dict[str, Any]]]: + """Helper method to get links from server for entity types. + + .. highlight:: text + .. code-block:: text + + Example output: + { + "59a212c0d2e211eda0e20242ac120001": [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ], + ... + } Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action - configuration. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + entity_type (Literal["folder", "task", "product", + "version", "representations"]): Entity type. + entity_ids (Optional[Iterable[str]]): Ids of entities for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. Returns: - ActionConfigResponse: New action configuration data. + dict[str, list[dict[str, Any]]]: Link info by entity ids. """ con = get_server_api_connection() - return con.set_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - value=value, + return con.get_entities_links( project_name=project_name, entity_type=entity_type, entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + link_types=link_types, + link_direction=link_direction, + link_names=link_names, + link_name_regex=link_name_regex, ) -def take_action( - action_token: str, -) -> "ActionTakeResponse": - """Take action metadata using an action token. +def get_folders_links( + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query folders links from server. Args: - action_token (str): AYON launcher action token. + project_name (str): Project where links are. + folder_ids (Optional[Iterable[str]]): Ids of folders for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionTakeResponse: Action metadata describing how to launch - action. + dict[str, list[dict[str, Any]]]: Link info by folder ids. """ con = get_server_api_connection() - return con.take_action( - action_token=action_token, + return con.get_folders_links( + project_name=project_name, + folder_ids=folder_ids, + link_types=link_types, + link_direction=link_direction, ) -def abort_action( - action_token: str, - message: Optional[str] = None, -) -> None: - """Abort action using an action token. +def get_folder_links( + project_name: str, + folder_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query folder links from server. Args: - action_token (str): AYON launcher action token. - message (Optional[str]): Message to display in the UI. + project_name (str): Project where links are. + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of folder. """ con = get_server_api_connection() - return con.abort_action( - action_token=action_token, - message=message, + return con.get_folder_links( + project_name=project_name, + folder_id=folder_id, + link_types=link_types, + link_direction=link_direction, ) -def get_addon_endpoint( - addon_name: str, - addon_version: str, - *subpaths, -) -> str: - """Calculate endpoint to addon route. - - Examples: - >>> from ayon_api import ServerAPI - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' +def get_tasks_links( + project_name: str, + task_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query tasks links from server. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. + project_name (str): Project where links are. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - str: Final url. + dict[str, list[dict[str, Any]]]: Link info by task ids. """ con = get_server_api_connection() - return con.get_addon_endpoint( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, + return con.get_tasks_links( + project_name=project_name, + task_ids=task_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_addons_info( - details: bool = True, -) -> "AddonsInfoDict": - """Get information about addons available on server. +def get_task_links( + project_name: str, + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query task links from server. Args: - details (Optional[bool]): Detailed data with information how - to get client code. + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of task. """ con = get_server_api_connection() - return con.get_addons_info( - details=details, + return con.get_task_links( + project_name=project_name, + task_id=task_id, + link_types=link_types, + link_direction=link_direction, ) -def get_addon_url( - addon_name: str, - addon_version: str, - *subpaths, - use_rest: bool = True, -) -> str: - """Calculate url to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' +def get_products_links( + project_name: str, + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - str: Final url. + dict[str, list[dict[str, Any]]]: Link info by product ids. """ con = get_server_api_connection() - return con.get_addon_url( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - use_rest=use_rest, + return con.get_products_links( + project_name=project_name, + product_ids=product_ids, + link_types=link_types, + link_direction=link_direction, ) -def delete_addon( - addon_name: str, - purge: Optional[bool] = None, -) -> None: - """Delete addon from server. - - Delete all versions of addon from server. +def get_product_links( + project_name: str, + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query product links from server. Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of product. """ con = get_server_api_connection() - return con.delete_addon( - addon_name=addon_name, - purge=purge, + return con.get_product_links( + project_name=project_name, + product_id=product_id, + link_types=link_types, + link_direction=link_direction, ) -def delete_addon_version( - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, -) -> None: - """Delete addon version from server. - - Delete all versions of addon from server. +def get_versions_links( + project_name: str, + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by version ids. """ con = get_server_api_connection() - return con.delete_addon_version( - addon_name=addon_name, - addon_version=addon_version, - purge=purge, + return con.get_versions_links( + project_name=project_name, + version_ids=version_ids, + link_types=link_types, + link_direction=link_direction, ) -def upload_addon_zip( - src_filepath: str, - progress: Optional[TransferProgress] = None, -): - """Upload addon zip file to server. +def get_version_links( + project_name: str, + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query version links from server. - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). + Args: + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. - Example output:: + Returns: + list[dict[str, Any]]: Link info of version. - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + """ + con = get_server_api_connection() + return con.get_version_links( + project_name=project_name, + version_id=version_id, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_representations_links( + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query representations links from server. Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. + project_name (str): Project where links are. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - dict[str, Any]: Response data from server. + dict[str, list[dict[str, Any]]]: Link info by representation ids. """ con = get_server_api_connection() - return con.upload_addon_zip( - src_filepath=src_filepath, - progress=progress, + return con.get_representations_links( + project_name=project_name, + representation_ids=representation_ids, + link_types=link_types, + link_direction=link_direction, ) -def download_addon_private_file( - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. +def get_representation_links( + project_name: str, + representation_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query representation links from server. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + project_name (str): Project where links are. + representation_id (str): Representation id for which links + should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - str: Filepath to downloaded file. + list[dict[str, Any]]: Link info of representation. """ con = get_server_api_connection() - return con.download_addon_private_file( - addon_name=addon_name, - addon_version=addon_version, - filename=filename, - destination_dir=destination_dir, - destination_filename=destination_filename, - chunk_size=chunk_size, - progress=progress, + return con.get_representation_links( + project_name=project_name, + representation_id=representation_id, + link_types=link_types, + link_direction=link_direction, ) From 41890a0d6b064d6dda8a2a29cfa1006fd56ac903 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:33:56 +0200 Subject: [PATCH 128/506] move 'prepare_list_filters' to utils --- ayon_api/server_api.py | 15 ++++++++------- ayon_api/utils.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 80dc669aa..35c3a1282 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -103,6 +103,8 @@ SortOrder, get_machine_name, fill_own_attribs, + prepare_list_filters, + PatternType, ) from ._actions import _ActionsAPI from ._addons import _AddonsAPI @@ -143,7 +145,6 @@ StreamType, ) -PatternType = type(re.compile("")) JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) _PLACEHOLDER = object() @@ -1583,7 +1584,7 @@ def get_events( statuses = states filters = {} - if not _prepare_list_filters( + if not prepare_list_filters( filters, ("eventTopics", topics), ("eventIds", event_ids), @@ -1923,7 +1924,7 @@ def get_activities( if reference_types is None: reference_types = {"origin"} - if not _prepare_list_filters( + if not prepare_list_filters( filters, ("activityIds", activity_ids), ("activityTypes", activity_types), @@ -4884,7 +4885,7 @@ def get_tasks( filters = { "projectName": project_name } - if not _prepare_list_filters( + if not prepare_list_filters( filters, ("taskIds", task_ids), ("taskNames", task_names), @@ -5040,7 +5041,7 @@ def get_tasks_by_folder_paths( "projectName": project_name, "folderPaths": list(folder_paths), } - if not _prepare_list_filters( + if not prepare_list_filters( filters, ("taskNames", task_names), ("taskTypes", task_types), @@ -5451,7 +5452,7 @@ def get_products( if filter_product_names: filters["productNames"] = list(filter_product_names) - if not _prepare_list_filters( + if not prepare_list_filters( filters, ("productIds", product_ids), ("productTypes", product_types), @@ -5872,7 +5873,7 @@ def get_versions( filters = { "projectName": project_name } - if not _prepare_list_filters( + if not prepare_list_filters( filters, ("taskIds", task_ids), ("versionIds", version_ids), diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 1f863233c..dc5349ef7 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import datetime @@ -7,6 +9,7 @@ import platform import traceback import collections +import itertools from urllib.parse import urlparse, urlencode import typing from typing import Optional, Dict, Set, Any, Iterable @@ -31,6 +34,8 @@ SLUGIFY_WHITELIST = string.ascii_letters + string.digits SLUGIFY_SEP_WHITELIST = " ,./\\;:!|*^#@~+-_=" +PatternType = type(re.compile("")) + RepresentationParents = collections.namedtuple( "RepresentationParents", ("version", "product", "folder", "project") @@ -112,6 +117,31 @@ def fill_own_attribs(entity: "AnyEntityDict") -> None: own_attrib[key] = copy.deepcopy(value) +def _convert_list_filter_value(value: Any) -> Optional[list[Any]]: + if value is None: + return None + + if isinstance(value, PatternType): + return [value.pattern] + + if isinstance(value, (int, float, str, bool)): + return [value] + return list(set(value)) + + +def prepare_list_filters( + output: dict[str, Any], *args: tuple[str, Any], **kwargs: Any +) -> bool: + for key, value in itertools.chain(args, kwargs.items()): + value = _convert_list_filter_value(value) + if value is None: + continue + if not value: + return False + output[key] = value + return True + + def get_default_timeout() -> float: """Default value for requests timeout. From 531b95e6c84301e0b7ea8eabeb6a626de90e41c0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:34:28 +0200 Subject: [PATCH 129/506] define 'ServerVersion' type --- ayon_api/_base.py | 4 +++- ayon_api/server_api.py | 5 +++-- ayon_api/typing.py | 2 ++ 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index 889617a51..f8cf506e0 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -8,10 +8,12 @@ from .utils import TransferProgress, RequestType if typing.TYPE_CHECKING: - from .typing import AnyEntityDict + from .typing import AnyEntityDict, ServerVersion class _BaseServerAPI: + def get_server_version_tuple(self) -> "ServerVersion": + raise NotImplementedError() def get_base_url(self) -> str: raise NotImplementedError() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 35c3a1282..a7f378aa2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -115,6 +115,7 @@ if typing.TYPE_CHECKING: from typing import Union from .typing import ( + ServerVersion, ActivityType, ActivityReferenceType, LinkDirection, @@ -1048,7 +1049,7 @@ def get_server_version(self) -> str: self._server_version = self.get_info()["version"] return self._server_version - def get_server_version_tuple(self) -> Tuple[int, int, int, str, str]: + def get_server_version_tuple(self) -> "ServerVersion": """Get server version as tuple. Version should match semantic version (https://semver.org/). @@ -1073,7 +1074,7 @@ def get_server_version_tuple(self) -> Tuple[int, int, int, str, str]: return self._server_version_tuple server_version = property(get_server_version) - server_version_tuple: Tuple[int, int, int, str, str] = property( + server_version_tuple: "ServerVersion" = property( get_server_version_tuple ) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 32c582bea..541ed734d 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -11,6 +11,8 @@ ) +ServerVersion = tuple[int, int, int, str, str] + ActivityType = Literal[ "comment", "watch", From eaf901e582fca8664d1cb777ae86bdb780289391 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:34:39 +0200 Subject: [PATCH 130/506] implement 'get_rest_entity_by_id' --- ayon_api/_base.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index f8cf506e0..faf8c81bf 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -14,6 +14,7 @@ class _BaseServerAPI: def get_server_version_tuple(self) -> "ServerVersion": raise NotImplementedError() + def get_base_url(self) -> str: raise NotImplementedError() @@ -75,6 +76,14 @@ def download_file( ) -> TransferProgress: raise NotImplementedError() + def get_rest_entity_by_id( + self, + project_name: str, + entity_type: str, + entity_id: str, + ) -> Optional["AnyEntityDict"]: + raise NotImplementedError() + def _prepare_fields( self, entity_type: str, From ec5f5e6286f2814e97041b377ee6c9caa6e94d11 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 18:44:29 +0200 Subject: [PATCH 131/506] move folders to separate class --- automated_api.py | 2 + ayon_api/__init__.py | 48 +- ayon_api/_api.py | 3797 ++++++++++++++++++++-------------------- ayon_api/_folders.py | 637 +++++++ ayon_api/server_api.py | 640 +------ 5 files changed, 2564 insertions(+), 2560 deletions(-) create mode 100644 ayon_api/_folders.py diff --git a/automated_api.py b/automated_api.py index a5971552e..64ec6bd92 100644 --- a/automated_api.py +++ b/automated_api.py @@ -337,6 +337,7 @@ def prepare_api_functions(api_globals): ServerAPI, _ActionsAPI, _AddonsAPI, + _FoldersAPI, _LinksAPI, _ListsAPI, _ProjectsAPI, @@ -346,6 +347,7 @@ def prepare_api_functions(api_globals): _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) + _items.extend(_FoldersAPI.__dict__.items()) _items.extend(_LinksAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) _items.extend(_ProjectsAPI.__dict__.items()) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index a7c41c734..11bf7df5b 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -138,22 +138,10 @@ save_secret, delete_secret, get_rest_entity_by_id, - get_rest_folder, - get_rest_folders, get_rest_task, get_rest_product, get_rest_version, get_rest_representation, - get_folders_hierarchy, - get_folders_rest, - get_folders, - get_folder_by_id, - get_folder_by_path, - get_folder_by_name, - get_folder_ids_with_products, - create_folder, - update_folder, - delete_folder, get_tasks, get_task_by_name, get_task_by_id, @@ -222,6 +210,18 @@ delete_addon_version, upload_addon_zip, download_addon_private_file, + get_rest_folder, + get_rest_folders, + get_folders_hierarchy, + get_folders_rest, + get_folders, + get_folder_by_id, + get_folder_by_path, + get_folder_by_name, + get_folder_ids_with_products, + create_folder, + update_folder, + delete_folder, get_full_link_type_name, get_link_types, get_link_type, @@ -402,22 +402,10 @@ "save_secret", "delete_secret", "get_rest_entity_by_id", - "get_rest_folder", - "get_rest_folders", "get_rest_task", "get_rest_product", "get_rest_version", "get_rest_representation", - "get_folders_hierarchy", - "get_folders_rest", - "get_folders", - "get_folder_by_id", - "get_folder_by_path", - "get_folder_by_name", - "get_folder_ids_with_products", - "create_folder", - "update_folder", - "delete_folder", "get_tasks", "get_task_by_name", "get_task_by_id", @@ -486,6 +474,18 @@ "delete_addon_version", "upload_addon_zip", "download_addon_private_file", + "get_rest_folder", + "get_rest_folders", + "get_folders_hierarchy", + "get_folders_rest", + "get_folders", + "get_folder_by_id", + "get_folder_by_path", + "get_folder_by_name", + "get_folder_ids_with_products", + "create_folder", + "update_folder", + "delete_folder", "get_full_link_type_name", "get_link_types", "get_link_type", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 26105e319..db11667a1 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -39,6 +39,7 @@ if typing.TYPE_CHECKING: from typing import Union from .typing import ( + ServerVersion, ActivityType, ActivityReferenceType, EntityListEntityType, @@ -696,7 +697,7 @@ def get_server_version() -> str: return con.get_server_version() -def get_server_version_tuple() -> Tuple[int, int, int, str, str]: +def get_server_version_tuple() -> "ServerVersion": """Get server version as tuple. Version should match semantic version (https://semver.org/). @@ -3048,68 +3049,6 @@ def get_rest_entity_by_id( ) -def get_rest_folder( - project_name: str, - folder_id: str, -) -> Optional["FolderDict"]: - con = get_server_api_connection() - return con.get_rest_folder( - project_name=project_name, - folder_id=folder_id, - ) - - -def get_rest_folders( - project_name: str, - include_attrib: bool = False, -) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] - - Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. - - Returns: - List[FlatFolderDict]: List of folder entities. - - """ - con = get_server_api_connection() - return con.get_rest_folders( - project_name=project_name, - include_attrib=include_attrib, - ) - - def get_rest_task( project_name: str, task_id: str, @@ -3154,165 +3093,41 @@ def get_rest_representation( ) -def get_folders_hierarchy( - project_name: str, - search_string: Optional[str] = None, - folder_types: Optional[Iterable[str]] = None, -) -> "ProjectHierarchyDict": - """Get project hierarchy. - - All folders in project in hierarchy data structure. - - Example output: - { - "hierarchy": [ - { - "id": "...", - "name": "...", - "label": "...", - "status": "...", - "folderType": "...", - "hasTasks": False, - "taskNames": [], - "parents": [], - "parentId": None, - "children": [...children folders...] - }, - ... - ] - } - - Args: - project_name (str): Project where to look for folders. - search_string (Optional[str]): Search string to filter folders. - folder_types (Optional[Iterable[str]]): Folder types to filter. - - Returns: - dict[str, Any]: Response data from server. - - """ - con = get_server_api_connection() - return con.get_folders_hierarchy( - project_name=project_name, - search_string=search_string, - folder_types=folder_types, - ) - - -def get_folders_rest( - project_name: str, - include_attrib: bool = False, -) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] - - Deprecated: - Use 'get_rest_folders' instead. Function was renamed to match - other rest functions, like 'get_rest_folder', - 'get_rest_project' etc. . - Will be removed in '1.0.7' or '1.1.0'. - - Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. - - Returns: - List[FlatFolderDict]: List of folder entities. - - """ - con = get_server_api_connection() - return con.get_folders_rest( - project_name=project_name, - include_attrib=include_attrib, - ) - - -def get_folders( +def get_tasks( project_name: str, + task_ids: Optional[Iterable[str]] = None, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, folder_ids: Optional[Iterable[str]] = None, - folder_paths: Optional[Iterable[str]] = None, - folder_names: Optional[Iterable[str]] = None, - folder_types: Optional[Iterable[str]] = None, - parent_ids: Optional[Iterable[str]] = None, - folder_path_regex: Optional[str] = None, - has_products: Optional[bool] = None, - has_tasks: Optional[bool] = None, - has_children: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: "Union[bool, None]" = True, - has_links: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Generator["FolderDict", None, None]: - """Query folders from server. - - Todos: - Folder name won't be unique identifier, so we should add - folder path filtering. - - Notes: - Filter 'active' don't have direct filter in GraphQl. +) -> Generator["TaskDict", None, None]: + """Query task entities from server. Args: project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Folder ids to filter. - folder_paths (Optional[Iterable[str]]): Folder paths used - for filtering. - folder_names (Optional[Iterable[str]]): Folder names used - for filtering. - folder_types (Optional[Iterable[str]]): Folder types used - for filtering. - parent_ids (Optional[Iterable[str]]): Ids of folder parents. - Use 'None' if folder is direct child of project. - folder_path_regex (Optional[str]): Folder path regex used - for filtering. - has_products (Optional[bool]): Filter folders with/without - products. Ignored when None, default behavior. - has_tasks (Optional[bool]): Filter folders with/without - tasks. Ignored when None, default behavior. - has_children (Optional[bool]): Filter folders with/without - children. Ignored when None, default behavior. - statuses (Optional[Iterable[str]]): Folder statuses used - for filtering. - assignees_all (Optional[Iterable[str]]): Filter by assigness - on children tasks. Task must have all of passed assignees. - tags (Optional[Iterable[str]]): Folder tags used - for filtering. - active (Optional[bool]): Filter active/inactive folders. + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -3320,227 +3135,308 @@ def get_folders( not explicitly set on entity will have 'None' value. Returns: - Generator[FolderDict, None, None]: Queried folder entities. + Generator[TaskDict, None, None]: Queried task entities. """ con = get_server_api_connection() - return con.get_folders( + return con.get_tasks( project_name=project_name, + task_ids=task_ids, + task_names=task_names, + task_types=task_types, folder_ids=folder_ids, - folder_paths=folder_paths, - folder_names=folder_names, - folder_types=folder_types, - parent_ids=parent_ids, - folder_path_regex=folder_path_regex, - has_products=has_products, - has_tasks=has_tasks, - has_children=has_children, - statuses=statuses, + assignees=assignees, assignees_all=assignees_all, + statuses=statuses, tags=tags, active=active, - has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_folder_by_id( +def get_task_by_name( project_name: str, folder_id: str, + task_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by id. +) -> Optional["TaskDict"]: + """Query task entity by name and folder id. Args: project_name (str): Name of project where to look for queried entities. folder_id (str): Folder id. + task_name (str): Task name fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): Attribute values that are not explicitly set on entity will have 'None' value. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_folder_by_id( + return con.get_task_by_name( project_name=project_name, folder_id=folder_id, + task_name=task_name, fields=fields, own_attributes=own_attributes, ) -def get_folder_by_path( +def get_task_by_id( project_name: str, - folder_path: str, + task_id: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by path. - - Folder path is a path to folder with all parent names joined by slash. +) -> Optional["TaskDict"]: + """Query task entity by id. Args: project_name (str): Name of project where to look for queried entities. - folder_path (str): Folder path. + task_id (str): Task id. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): Attribute values that are not explicitly set on entity will have 'None' value. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_folder_by_path( + return con.get_task_by_id( project_name=project_name, - folder_path=folder_path, + task_id=task_id, fields=fields, own_attributes=own_attributes, ) -def get_folder_by_name( +def get_tasks_by_folder_paths( project_name: str, - folder_name: str, + folder_paths: Iterable[str], + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: "Union[bool, None]" = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by path. - - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. +) -> Dict[str, List["TaskDict"]]: + """Query task entities from server by folder paths. Args: - project_name (str): Name of project where to look for queried - entities. - folder_name (str): Folder name. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. + project_name (str): Name of project. + folder_paths (list[str]): Folder paths. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. own_attributes (Optional[bool]): Attribute values that are not explicitly set on entity will have 'None' value. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + Dict[str, List[TaskDict]]: Task entities by + folder path. """ con = get_server_api_connection() - return con.get_folder_by_name( + return con.get_tasks_by_folder_paths( project_name=project_name, - folder_name=folder_name, + folder_paths=folder_paths, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, fields=fields, own_attributes=own_attributes, ) -def get_folder_ids_with_products( +def get_tasks_by_folder_path( project_name: str, - folder_ids: Optional[Iterable[str]] = None, -) -> Set[str]: - """Find folders which have at least one product. - - Folders that have at least one product should be immutable, so they - should not change path -> change of name or name of any parent - is not possible. + folder_path: str, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: "Union[bool, None]" = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> List["TaskDict"]: + """Query task entities from server by folder path. Args: project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Limit folder ids filtering - to a set of folders. If set to None all folders on project are - checked. + folder_path (str): Folder path. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + """ + con = get_server_api_connection() + return con.get_tasks_by_folder_path( + project_name=project_name, + folder_path=folder_path, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_task_by_folder_path( + project_name: str, + folder_path: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by folder path and task name. + + Args: + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + fields (Optional[Iterable[str]]): Task fields that should + be returned. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - set[str]: Folder ids that have at least one product. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_folder_ids_with_products( + return con.get_task_by_folder_path( project_name=project_name, - folder_ids=folder_ids, + folder_path=folder_path, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, ) -def create_folder( +def create_task( project_name: str, name: str, - folder_type: Optional[str] = None, - parent_id: Optional[str] = None, + task_type: str, + folder_id: str, label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = None, - folder_id: Optional[str] = None, + task_id: Optional[str] = None, ) -> str: - """Create new folder. + """Create new task. Args: project_name (str): Project name. name (str): Folder name. - folder_type (Optional[str]): Folder type. - parent_id (Optional[str]): Parent folder id. Parent is project - if is ``None``. + task_type (str): Task type. + folder_id (str): Parent folder id. label (Optional[str]): Label of folder. - attrib (Optional[dict[str, Any]]): Folder attributes. - data (Optional[dict[str, Any]]): Folder data. - tags (Optional[Iterable[str]]): Folder tags. - status (Optional[str]): Folder status. - active (Optional[bool]): Folder active state. - thumbnail_id (Optional[str]): Folder thumbnail id. - folder_id (Optional[str]): Folder id. If not passed new id is + assignees (Optional[Iterable[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[Iterable[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is generated. Returns: - str: Entity id. + str: Task id. """ con = get_server_api_connection() - return con.create_folder( + return con.create_task( project_name=project_name, name=name, - folder_type=folder_type, - parent_id=parent_id, + task_type=task_type, + folder_id=folder_id, label=label, + assignees=assignees, attrib=attrib, data=data, tags=tags, status=status, active=active, thumbnail_id=thumbnail_id, - folder_id=folder_id, + task_id=task_id, ) -def update_folder( +def update_task( project_name: str, - folder_id: str, + task_id: str, name: Optional[str] = None, - folder_type: Optional[str] = None, - parent_id: Optional[str] = NOT_SET, + task_type: Optional[str] = None, + folder_id: Optional[str] = None, label: Optional[str] = NOT_SET, + assignees: Optional[List[str]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, ): - """Update folder entity on server. + """Update task entity on server. - Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't + Do not pass ``label`` amd ``thumbnail_id`` if you don't want to change their values. Value ``None`` would unset their value. @@ -3551,11 +3447,12 @@ def update_folder( Args: project_name (str): Project name. - folder_id (str): Folder id. + task_id (str): Task id. name (Optional[str]): New name. - folder_type (Optional[str]): New folder type. - parent_id (Optional[Union[str, None]]): New parent folder id. + task_type (Optional[str]): New task type. + folder_id (Optional[str]): New folder id. label (Optional[Union[str, None]]): New label. + assignees (Optional[str]): New assignees. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. @@ -3565,13 +3462,14 @@ def update_folder( """ con = get_server_api_connection() - return con.update_folder( + return con.update_task( project_name=project_name, - folder_id=folder_id, + task_id=task_id, name=name, - folder_type=folder_type, - parent_id=parent_id, + task_type=task_type, + folder_id=folder_id, label=label, + assignees=assignees, attrib=attrib, data=data, tags=tags, @@ -3581,82 +3479,85 @@ def update_folder( ) -def delete_folder( +def delete_task( project_name: str, - folder_id: str, - force: bool = False, + task_id: str, ): - """Delete folder. + """Delete task. Args: project_name (str): Project name. - folder_id (str): Folder id to delete. - force (Optional[bool]): Folder delete folder with all children - folder, products, versions and representations. + task_id (str): Task id to delete. """ con = get_server_api_connection() - return con.delete_folder( + return con.delete_task( project_name=project_name, - folder_id=folder_id, - force=force, + task_id=task_id, ) -def get_tasks( +def get_products( project_name: str, - task_ids: Optional[Iterable[str]] = None, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, + product_ids: Optional[Iterable[str]] = None, + product_names: Optional[Iterable[str]] = None, folder_ids: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, + product_types: Optional[Iterable[str]] = None, + product_name_regex: Optional[str] = None, + product_path_regex: Optional[str] = None, + names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: "Union[bool, None]" = True, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["TaskDict", None, None]: - """Query task entities from server. + own_attributes=_PLACEHOLDER, +) -> Generator["ProductDict", None, None]: + """Query products from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. Args: project_name (str): Name of project. - task_ids (Iterable[str]): Task ids to filter. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for filtering. - tags (Optional[Iterable[str]]): Task tags used for + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for filtering. - active (Optional[bool]): Filter active/inactive tasks. + product_name_regex (Optional[str]): Filter products by name regex. + product_path_regex (Optional[str]): Filter products by path regex. + Path starts with folder path and ends with product name. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + statuses (Optional[Iterable[str]]): Product statuses used + for filtering. + tags (Optional[Iterable[str]]): Product tags used + for filtering. + active (Optional[bool]): Filter active/inactive products. Both are returned if is set to None. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - Generator[TaskDict, None, None]: Queried task entities. + Generator[ProductDict, None, None]: Queried product entities. """ con = get_server_api_connection() - return con.get_tasks( + return con.get_products( project_name=project_name, - task_ids=task_ids, - task_names=task_names, - task_types=task_types, + product_ids=product_ids, + product_names=product_names, folder_ids=folder_ids, - assignees=assignees, - assignees_all=assignees_all, + product_types=product_types, + product_name_regex=product_name_regex, + product_path_regex=product_path_regex, + names_by_folder_ids=names_by_folder_ids, statuses=statuses, tags=tags, active=active, @@ -3665,290 +3566,202 @@ def get_tasks( ) -def get_task_by_name( +def get_product_by_id( project_name: str, - folder_id: str, - task_name: str, + product_id: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by name and folder id. + own_attributes=_PLACEHOLDER, +) -> Optional["ProductDict"]: + """Query product entity by id. Args: project_name (str): Name of project where to look for queried entities. - folder_id (str): Folder id. - task_name (str): Task name + product_id (str): Product id. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_task_by_name( + return con.get_product_by_id( project_name=project_name, - folder_id=folder_id, - task_name=task_name, + product_id=product_id, fields=fields, own_attributes=own_attributes, ) -def get_task_by_id( +def get_product_by_name( project_name: str, - task_id: str, + product_name: str, + folder_id: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by id. + own_attributes=_PLACEHOLDER, +) -> Optional["ProductDict"]: + """Query product entity by name and folder id. Args: project_name (str): Name of project where to look for queried entities. - task_id (str): Task id. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_task_by_id( + return con.get_product_by_name( project_name=project_name, - task_id=task_id, + product_name=product_name, + folder_id=folder_id, fields=fields, own_attributes=own_attributes, ) -def get_tasks_by_folder_paths( - project_name: str, - folder_paths: Iterable[str], - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, +def get_product_types( fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Dict[str, List["TaskDict"]]: - """Query task entities from server by folder paths. +) -> List["ProductTypeDict"]: + """Types of products. + + This is server wide information. Product types have 'name', 'icon' and + 'color'. Args: - project_name (str): Name of project. - folder_paths (list[str]): Folder paths. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - Dict[str, List[TaskDict]]: Task entities by - folder path. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_tasks_by_folder_paths( - project_name=project_name, - folder_paths=folder_paths, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, + return con.get_product_types( fields=fields, - own_attributes=own_attributes, ) -def get_tasks_by_folder_path( +def get_project_product_types( project_name: str, - folder_path: str, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> List["TaskDict"]: - """Query task entities from server by folder path. +) -> List["ProductTypeDict"]: + """DEPRECATED Types of products available in a project. + + Filter only product types available in a project. Args: - project_name (str): Name of project. - folder_path (str): Folder path. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Name of the project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + List[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_tasks_by_folder_path( + return con.get_project_product_types( project_name=project_name, - folder_path=folder_path, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, fields=fields, - own_attributes=own_attributes, ) -def get_task_by_folder_path( - project_name: str, - folder_path: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by folder path and task name. +def get_product_type_names( + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, +) -> Set[str]: + """DEPRECATED Product type names. - Args: - project_name (str): Project name. - folder_path (str): Folder path. - task_name (str): Task name. - fields (Optional[Iterable[str]]): Task fields that should - be returned. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. + + Args: + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + set[str]: Product type names. """ con = get_server_api_connection() - return con.get_task_by_folder_path( + return con.get_product_type_names( project_name=project_name, - folder_path=folder_path, - task_name=task_name, - fields=fields, - own_attributes=own_attributes, + product_ids=product_ids, ) -def create_task( +def create_product( project_name: str, name: str, - task_type: str, + product_type: str, folder_id: str, - label: Optional[str] = None, - assignees: Optional[Iterable[str]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - task_id: Optional[str] = None, + active: "Union[bool, None]" = None, + product_id: Optional[str] = None, ) -> str: - """Create new task. + """Create new product. Args: project_name (str): Project name. - name (str): Folder name. - task_type (str): Task type. + name (str): Product name. + product_type (str): Product type. folder_id (str): Parent folder id. - label (Optional[str]): Label of folder. - assignees (Optional[Iterable[str]]): Task assignees. - attrib (Optional[dict[str, Any]]): Task attributes. - data (Optional[dict[str, Any]]): Task data. - tags (Optional[Iterable[str]]): Task tags. - status (Optional[str]): Task status. - active (Optional[bool]): Task active state. - thumbnail_id (Optional[str]): Task thumbnail id. - task_id (Optional[str]): Task id. If not passed new id is + attrib (Optional[dict[str, Any]]): Product attributes. + data (Optional[dict[str, Any]]): Product data. + tags (Optional[Iterable[str]]): Product tags. + status (Optional[str]): Product status. + active (Optional[bool]): Product active state. + product_id (Optional[str]): Product id. If not passed new id is generated. Returns: - str: Task id. + str: Product id. """ con = get_server_api_connection() - return con.create_task( + return con.create_product( project_name=project_name, name=name, - task_type=task_type, + product_type=product_type, folder_id=folder_id, - label=label, - assignees=assignees, attrib=attrib, data=data, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, - task_id=task_id, + product_id=product_id, ) -def update_task( +def update_product( project_name: str, - task_id: str, + product_id: str, name: Optional[str] = None, - task_type: Optional[str] = None, folder_id: Optional[str] = None, - label: Optional[str] = NOT_SET, - assignees: Optional[List[str]] = None, + product_type: Optional[str] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, ): - """Update task entity on server. - - Do not pass ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. + """Update product entity on server. Update of ``data`` will override existing value on folder entity. @@ -3957,117 +3770,108 @@ def update_task( Args: project_name (str): Project name. - task_id (str): Task id. - name (Optional[str]): New name. - task_type (Optional[str]): New task type. - folder_id (Optional[str]): New folder id. - label (Optional[Union[str, None]]): New label. - assignees (Optional[str]): New assignees. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + product_id (str): Product id. + name (Optional[str]): New product name. + folder_id (Optional[str]): New product id. + product_type (Optional[str]): New product type. + attrib (Optional[dict[str, Any]]): New product attributes. + data (Optional[dict[str, Any]]): New product data. + tags (Optional[Iterable[str]]): New product tags. + status (Optional[str]): New product status. + active (Optional[bool]): New product active state. """ con = get_server_api_connection() - return con.update_task( + return con.update_product( project_name=project_name, - task_id=task_id, + product_id=product_id, name=name, - task_type=task_type, folder_id=folder_id, - label=label, - assignees=assignees, + product_type=product_type, attrib=attrib, data=data, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, ) -def delete_task( +def delete_product( project_name: str, - task_id: str, + product_id: str, ): - """Delete task. + """Delete product. Args: project_name (str): Project name. - task_id (str): Task id to delete. + product_id (str): Product id to delete. """ con = get_server_api_connection() - return con.delete_task( + return con.delete_product( project_name=project_name, - task_id=task_id, + product_id=product_id, ) -def get_products( +def get_versions( project_name: str, + version_ids: Optional[Iterable[str]] = None, product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - product_types: Optional[Iterable[str]] = None, - product_name_regex: Optional[str] = None, - product_path_regex: Optional[str] = None, - names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, + task_ids: Optional[Iterable[str]] = None, + versions: Optional[Iterable[str]] = None, + hero: bool = True, + standard: bool = True, + latest: Optional[bool] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: "Union[bool, None]" = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["ProductDict", None, None]: - """Query products from server. - - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. +) -> Generator["VersionDict", None, None]: + """Get version entities based on passed filters from server. Args: - project_name (str): Name of project. - product_ids (Optional[Iterable[str]]): Task ids to filter. - product_names (Optional[Iterable[str]]): Task names used for - filtering. - folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. - product_types (Optional[Iterable[str]]): Product types used for - filtering. - product_name_regex (Optional[str]): Filter products by name regex. - product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. - names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product - name filtering by folder id. - statuses (Optional[Iterable[str]]): Product statuses used + project_name (str): Name of project where to look for versions. + version_ids (Optional[Iterable[str]]): Version ids used for + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + task_ids (Optional[Iterable[str]]): Task ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Skip hero versions when set to False. + standard (Optional[bool]): Skip standard (non-hero) when + set to False. + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute + set to True. + statuses (Optional[Iterable[str]]): Representation statuses used for filtering. - tags (Optional[Iterable[str]]): Product tags used + tags (Optional[Iterable[str]]): Representation tags used for filtering. - active (Optional[bool]): Filter active/inactive products. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields to be queried + for version. All possible folder fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + versions. Returns: - Generator[ProductDict, None, None]: Queried product entities. + Generator[VersionDict, None, None]: Queried version entities. """ con = get_server_api_connection() - return con.get_products( + return con.get_versions( project_name=project_name, + version_ids=version_ids, product_ids=product_ids, - product_names=product_names, - folder_ids=folder_ids, - product_types=product_types, - product_name_regex=product_name_regex, - product_path_regex=product_path_regex, - names_by_folder_ids=names_by_folder_ids, + task_ids=task_ids, + versions=versions, + hero=hero, + standard=standard, + latest=latest, statuses=statuses, tags=tags, active=active, @@ -4076,683 +3880,833 @@ def get_products( ) -def get_product_by_id( +def get_version_by_id( project_name: str, - product_id: str, + version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by id. +) -> Optional["VersionDict"]: + """Query version entity by id. Args: project_name (str): Name of project where to look for queried entities. - product_id (str): Product id. + version_id (str): Version id. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + versions. Returns: - Optional[ProductDict]: Product entity data or None + Optional[VersionDict]: Version entity data or None if was not found. """ con = get_server_api_connection() - return con.get_product_by_id( + return con.get_version_by_id( project_name=project_name, - product_id=product_id, + version_id=version_id, fields=fields, own_attributes=own_attributes, ) -def get_product_by_name( +def get_version_by_name( project_name: str, - product_name: str, - folder_id: str, + version: int, + product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by name and folder id. +) -> Optional["VersionDict"]: + """Query version entity by version and product id. Args: project_name (str): Name of project where to look for queried entities. - product_name (str): Product name. - folder_id (str): Folder id (Folder is a parent of products). + version (int): Version of version entity. + product_id (str): Product id. Product is a parent of version. fields (Optional[Iterable[str]]): Fields that should be returned. All fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + versions. Returns: - Optional[ProductDict]: Product entity data or None + Optional[VersionDict]: Version entity data or None if was not found. """ con = get_server_api_connection() - return con.get_product_by_name( + return con.get_version_by_name( project_name=project_name, - product_name=product_name, - folder_id=folder_id, + version=version, + product_id=product_id, fields=fields, own_attributes=own_attributes, ) -def get_product_types( +def get_hero_version_by_id( + project_name: str, + version_id: str, fields: Optional[Iterable[str]] = None, -) -> List["ProductTypeDict"]: - """Types of products. - - This is server wide information. Product types have 'name', 'icon' and - 'color'. + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query hero version entity by id. Args: - fields (Optional[Iterable[str]]): Product types fields to query. + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - list[ProductTypeDict]: Product types information. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_product_types( + return con.get_hero_version_by_id( + project_name=project_name, + version_id=version_id, fields=fields, + own_attributes=own_attributes, ) -def get_project_product_types( +def get_hero_version_by_product_id( project_name: str, + product_id: str, fields: Optional[Iterable[str]] = None, -) -> List["ProductTypeDict"]: - """DEPRECATED Types of products available in a project. + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query hero version entity by product id. - Filter only product types available in a project. + Only one hero version is available on a product. Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. + project_name (str): Name of project where to look for queried + entities. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - List[ProductTypeDict]: Product types information. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_project_product_types( + return con.get_hero_version_by_product_id( project_name=project_name, + product_id=product_id, fields=fields, + own_attributes=own_attributes, ) -def get_product_type_names( - project_name: Optional[str] = None, +def get_hero_versions( + project_name: str, product_ids: Optional[Iterable[str]] = None, -) -> Set[str]: - """DEPRECATED Product type names. + version_ids: Optional[Iterable[str]] = None, + active: "Union[bool, None]" = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["VersionDict", None, None]: + """Query hero versions by multiple filters. - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. + Only one hero version is available on a product. Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. + project_name (str): Name of project where to look for queried + entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - set[str]: Product type names. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_product_type_names( + return con.get_hero_versions( project_name=project_name, product_ids=product_ids, + version_ids=version_ids, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def create_product( +def get_last_versions( project_name: str, - name: str, - product_type: str, - folder_id: str, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: "Union[bool, None]" = None, - product_id: Optional[str] = None, -) -> str: - """Create new product. + product_ids: Iterable[str], + active: "Union[bool, None]" = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Dict[str, Optional["VersionDict"]]: + """Query last version entities by product ids. Args: - project_name (str): Project name. - name (str): Product name. - product_type (str): Product type. - folder_id (str): Parent folder id. - attrib (Optional[dict[str, Any]]): Product attributes. - data (Optional[dict[str, Any]]): Product data. - tags (Optional[Iterable[str]]): Product tags. - status (Optional[str]): Product status. - active (Optional[bool]): Product active state. - product_id (Optional[str]): Product id. If not passed new id is - generated. + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - str: Product id. + dict[str, Optional[VersionDict]]: Last versions by product id. """ con = get_server_api_connection() - return con.create_product( + return con.get_last_versions( project_name=project_name, - name=name, - product_type=product_type, - folder_id=folder_id, - attrib=attrib, - data=data, - tags=tags, - status=status, + product_ids=product_ids, active=active, - product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def update_product( +def get_last_version_by_product_id( project_name: str, product_id: str, - name: Optional[str] = None, - folder_id: Optional[str] = None, - product_type: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, -): - """Update product entity on server. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + active: "Union[bool, None]" = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query last version entity by product id. Args: - project_name (str): Project name. + project_name (str): Project where to look for representation. product_id (str): Product id. - name (Optional[str]): New product name. - folder_id (Optional[str]): New product id. - product_type (Optional[str]): New product type. - attrib (Optional[dict[str, Any]]): New product attributes. - data (Optional[dict[str, Any]]): New product data. - tags (Optional[Iterable[str]]): New product tags. - status (Optional[str]): New product status. - active (Optional[bool]): New product active state. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.update_product( + return con.get_last_version_by_product_id( project_name=project_name, product_id=product_id, - name=name, - folder_id=folder_id, - product_type=product_type, - attrib=attrib, - data=data, - tags=tags, - status=status, active=active, + fields=fields, + own_attributes=own_attributes, ) -def delete_product( +def get_last_version_by_product_name( project_name: str, - product_id: str, -): - """Delete product. + product_name: str, + folder_id: str, + active: "Union[bool, None]" = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query last version entity by product name and folder id. Args: - project_name (str): Project name. - product_id (str): Product id to delete. + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.delete_product( + return con.get_last_version_by_product_name( project_name=project_name, - product_id=product_id, + product_name=product_name, + folder_id=folder_id, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_versions( +def version_is_latest( project_name: str, - version_ids: Optional[Iterable[str]] = None, - product_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - versions: Optional[Iterable[str]] = None, - hero: bool = True, - standard: bool = True, - latest: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Get version entities based on passed filters from server. + version_id: str, +) -> bool: + """Is version latest from a product. Args: - project_name (str): Name of project where to look for versions. - version_ids (Optional[Iterable[str]]): Version ids used for - version filtering. - product_ids (Optional[Iterable[str]]): Product ids used for - version filtering. - task_ids (Optional[Iterable[str]]): Task ids used for - version filtering. - versions (Optional[Iterable[int]]): Versions we're interested in. - hero (Optional[bool]): Skip hero versions when set to False. - standard (Optional[bool]): Skip standard (non-hero) when - set to False. - latest (Optional[bool]): Return only latest version of standard - versions. This can be combined only with 'standard' attribute - set to True. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields to be queried - for version. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where to look for representation. + version_id (str): Version id. Returns: - Generator[VersionDict, None, None]: Queried version entities. + bool: Version is latest or not. """ con = get_server_api_connection() - return con.get_versions( + return con.version_is_latest( project_name=project_name, - version_ids=version_ids, - product_ids=product_ids, - task_ids=task_ids, - versions=versions, - hero=hero, - standard=standard, - latest=latest, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + version_id=version_id, ) -def get_version_by_id( +def create_version( project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by id. + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + version_id: Optional[str] = None, +) -> str: + """Create new version. Args: - project_name (str): Name of project where to look for queried - entities. - version_id (str): Version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project name. + version (int): Version. + product_id (str): Parent product id. + task_id (Optional[str]): Parent task id. + author (Optional[str]): Version author. + attrib (Optional[dict[str, Any]]): Version attributes. + data (Optional[dict[str, Any]]): Version data. + tags (Optional[Iterable[str]]): Version tags. + status (Optional[str]): Version status. + active (Optional[bool]): Version active state. + thumbnail_id (Optional[str]): Version thumbnail id. + version_id (Optional[str]): Version id. If not passed new id is + generated. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + str: Version id. """ con = get_server_api_connection() - return con.get_version_by_id( + return con.create_version( project_name=project_name, + version=version, + product_id=product_id, + task_id=task_id, + author=author, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, version_id=version_id, - fields=fields, - own_attributes=own_attributes, ) -def get_version_by_name( +def update_version( project_name: str, - version: int, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by version and product id. + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + author: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, +): + """Update version entity on server. - Args: - project_name (str): Name of project where to look for queried - entities. - version (int): Version of version entity. - product_id (str): Product id. Product is a parent of version. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + version_id (str): Version id. + version (Optional[int]): New version. + product_id (Optional[str]): New product id. + task_id (Optional[Union[str, None]]): New task id. + author (Optional[str]): New author username. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[Union[str, None]]): New thumbnail id. """ con = get_server_api_connection() - return con.get_version_by_name( + return con.update_version( project_name=project_name, + version_id=version_id, version=version, product_id=product_id, - fields=fields, - own_attributes=own_attributes, + task_id=task_id, + author=author, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, ) -def get_hero_version_by_id( +def delete_version( project_name: str, version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by id. +): + """Delete version. Args: - project_name (str): Name of project where to look for queried - entities. - version_id (int): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + project_name (str): Project name. + version_id (str): Version id to delete. """ con = get_server_api_connection() - return con.get_hero_version_by_id( + return con.delete_version( project_name=project_name, version_id=version_id, - fields=fields, - own_attributes=own_attributes, ) -def get_hero_version_by_product_id( +def get_representations( project_name: str, - product_id: str, + representation_ids: Optional[Iterable[str]] = None, + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + names_by_version_ids: Optional[Dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: "Union[bool, None]" = True, + has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by product id. +) -> Generator["RepresentationDict", None, None]: + """Get representation entities based on passed filters from server. - Only one hero version is available on a product. + .. todo:: + + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. Args: - project_name (str): Name of project where to look for queried - entities. - product_id (int): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. + project_name (str): Name of project where to look for versions. + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (Optional[Dict[str, Iterable[str]]]): Find + representations by names and version ids. This filter + discards all other filters. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + representations. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Generator[RepresentationDict, None, None]: Queried + representation entities. """ con = get_server_api_connection() - return con.get_hero_version_by_product_id( + return con.get_representations( project_name=project_name, - product_id=product_id, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + names_by_version_ids=names_by_version_ids, + statuses=statuses, + tags=tags, + active=active, + has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_hero_versions( +def get_representation_by_id( project_name: str, - product_ids: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, + representation_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Query hero versions by multiple filters. - - Only one hero version is available on a product. +) -> Optional["RepresentationDict"]: + """Query representation entity from server based on id filter. Args: - project_name (str): Name of project where to look for queried - entities. - product_ids (Optional[Iterable[str]]): Product ids. - version_ids (Optional[Iterable[str]]): Version ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Optional[Iterable[str]]): fields to be queried + for representations. own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + representations. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Optional[RepresentationDict]: Queried representation + entity or None. """ con = get_server_api_connection() - return con.get_hero_versions( + return con.get_representation_by_id( project_name=project_name, - product_ids=product_ids, - version_ids=version_ids, - active=active, + representation_id=representation_id, fields=fields, own_attributes=own_attributes, ) -def get_last_versions( +def get_representation_by_name( project_name: str, - product_ids: Iterable[str], - active: "Union[bool, None]" = True, + representation_name: str, + version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Dict[str, Optional["VersionDict"]]: - """Query last version entities by product ids. +) -> Optional["RepresentationDict"]: + """Query representation entity by name and version id. Args: project_name (str): Project where to look for representation. - product_ids (Iterable[str]): Product ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. + representation_name (str): Representation name. + version_id (str): Version id. fields (Optional[Iterable[str]]): fields to be queried for representations. own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + representations. Returns: - dict[str, Optional[VersionDict]]: Last versions by product id. + Optional[RepresentationDict]: Queried representation entity + or None. """ con = get_server_api_connection() - return con.get_last_versions( + return con.get_representation_by_name( project_name=project_name, - product_ids=product_ids, - active=active, + representation_name=representation_name, + version_id=version_id, fields=fields, own_attributes=own_attributes, ) -def get_last_version_by_product_id( +def get_representations_hierarchy( project_name: str, - product_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product id. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> Dict[str, RepresentationHierarchy]: + """Find representation with parents by representation id. + + Representation entity with parent entities up to project. + + Default fields are used when any fields are set to `None`. But it is + possible to pass in empty iterable (list, set, tuple) to skip + entity. Args: - project_name (str): Project where to look for representation. - product_id (str): Product id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - Optional[VersionDict]: Queried version entity or None. + dict[str, RepresentationHierarchy]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_last_version_by_product_id( + return con.get_representations_hierarchy( project_name=project_name, - product_id=product_id, - active=active, - fields=fields, - own_attributes=own_attributes, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def get_last_version_by_product_name( +def get_representation_hierarchy( project_name: str, - product_name: str, - folder_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product name and folder id. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> Optional[RepresentationHierarchy]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project where to look for representation. - product_name (str): Product name. - folder_id (str): Folder id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - Optional[VersionDict]: Queried version entity or None. + RepresentationHierarchy: Representation hierarchy entities. """ con = get_server_api_connection() - return con.get_last_version_by_product_name( + return con.get_representation_hierarchy( project_name=project_name, - product_name=product_name, - folder_id=folder_id, - active=active, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def version_is_latest( +def get_representations_parents( project_name: str, - version_id: str, -) -> bool: - """Is version latest from a product. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> Dict[str, RepresentationParents]: + """Find representations parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - bool: Version is latest or not. + dict[str, RepresentationParents]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.version_is_latest( + return con.get_representations_parents( project_name=project_name, - version_id=version_id, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def create_version( +def get_representation_parents( project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, -) -> str: - """Create new version. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> Optional["RepresentationParents"]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project name. - version (int): Version. - product_id (str): Parent product id. - task_id (Optional[str]): Parent task id. - author (Optional[str]): Version author. - attrib (Optional[dict[str, Any]]): Version attributes. - data (Optional[dict[str, Any]]): Version data. - tags (Optional[Iterable[str]]): Version tags. - status (Optional[str]): Version status. - active (Optional[bool]): Version active state. - thumbnail_id (Optional[str]): Version thumbnail id. - version_id (Optional[str]): Version id. If not passed new id is - generated. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - str: Version id. + RepresentationParents: Representation parent entities. """ con = get_server_api_connection() - return con.create_version( + return con.get_representation_parents( project_name=project_name, - version=version, - product_id=product_id, - task_id=task_id, - author=author, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - version_id=version_id, - ) - + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, + ) -def update_version( + +def get_repre_ids_by_context_filters( + project_name: str, + context_filters: Optional[Dict[str, Iterable[str]]], + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, +) -> List[str]: + """Find representation ids which match passed context filters. + + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or ``re.Pattern`` can + be used. + + Args: + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. + + Returns: + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "product": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) + + """ + con = get_server_api_connection() + return con.get_repre_ids_by_context_filters( + project_name=project_name, + context_filters=context_filters, + representation_names=representation_names, + version_ids=version_ids, + ) + + +def create_representation( project_name: str, + name: str, version_id: str, - version: Optional[int] = None, - product_id: Optional[str] = None, - task_id: Optional[str] = NOT_SET, - author: Optional[str] = None, + files: Optional[List[Dict[str, Any]]] = None, attrib: Optional[Dict[str, Any]] = None, data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, + traits: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, -): - """Update version entity on server. + representation_id: Optional[str] = None, +) -> str: + """Create new representation. - Do not pass ``task_id`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. + Args: + project_name (str): Project name. + name (str): Representation name. + version_id (str): Parent version id. + files (Optional[list[dict]]): Representation files information. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + representation_id (Optional[str]): Representation id. If not + passed new id is generated. + + Returns: + str: Representation id. + + """ + con = get_server_api_connection() + return con.create_representation( + project_name=project_name, + name=name, + version_id=version_id, + files=files, + attrib=attrib, + data=data, + traits=traits, + tags=tags, + status=status, + active=active, + representation_id=representation_id, + ) + + +def update_representation( + project_name: str, + representation_id: str, + name: Optional[str] = None, + version_id: Optional[str] = None, + files: Optional[List[Dict[str, Any]]] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + traits: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, +): + """Update representation entity on server. Update of ``data`` will override existing value on folder entity. @@ -4761,1360 +4715,1407 @@ def update_version( Args: project_name (str): Project name. - version_id (str): Version id. - version (Optional[int]): New version. - product_id (Optional[str]): New product id. - task_id (Optional[Union[str, None]]): New task id. - author (Optional[str]): New author username. + representation_id (str): Representation id. + name (Optional[str]): New name. + version_id (Optional[str]): New version id. + files (Optional[list[dict]]): New files + information. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. + traits (Optional[dict[str, Any]]): New traits. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. """ con = get_server_api_connection() - return con.update_version( + return con.update_representation( project_name=project_name, + representation_id=representation_id, + name=name, version_id=version_id, - version=version, - product_id=product_id, - task_id=task_id, - author=author, + files=files, attrib=attrib, data=data, + traits=traits, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, ) -def delete_version( +def delete_representation( project_name: str, - version_id: str, + representation_id: str, ): - """Delete version. + """Delete representation. Args: project_name (str): Project name. - version_id (str): Version id to delete. + representation_id (str): Representation id to delete. """ con = get_server_api_connection() - return con.delete_version( + return con.delete_representation( project_name=project_name, - version_id=version_id, + representation_id=representation_id, ) -def get_representations( +def get_workfiles_info( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - names_by_version_ids: Optional[Dict[str, Iterable[str]]] = None, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + paths: Optional[Iterable[str]] = None, + path_regex: Optional[str] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["RepresentationDict", None, None]: - """Get representation entities based on passed filters from server. - - .. todo:: - - Add separated function for 'names_by_version_ids' filtering. - Because can't be combined with others. +) -> Generator["WorkfileInfoDict", None, None]: + """Workfile info entities by passed filters. Args: - project_name (str): Name of project where to look for versions. - representation_ids (Optional[Iterable[str]]): Representation ids - used for representation filtering. - representation_names (Optional[Iterable[str]]): Representation - names used for representation filtering. - version_ids (Optional[Iterable[str]]): Version ids used for - representation filtering. Versions are parents of - representations. - names_by_version_ids (Optional[Dict[str, Iterable[str]]]): Find - representations by names and version ids. This filter - discards all other filters. - statuses (Optional[Iterable[str]]): Representation statuses used + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used for filtering. - tags (Optional[Iterable[str]]): Representation tags used + tags (Optional[Iterable[str]]): Workfile info tags used for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. has_links (Optional[Literal[IN, OUT, ANY]]): Filter representations with IN/OUT/ANY links. fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + workfiles. Returns: - Generator[RepresentationDict, None, None]: Queried - representation entities. + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. """ con = get_server_api_connection() - return con.get_representations( + return con.get_workfiles_info( project_name=project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - names_by_version_ids=names_by_version_ids, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, statuses=statuses, tags=tags, - active=active, has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_representation_by_id( +def get_workfile_info( project_name: str, - representation_id: str, + task_id: str, + path: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity from server based on id filter. +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by task id and workfile path. Args: - project_name (str): Project where to look for representation. - representation_id (str): Id of representation. - fields (Optional[Iterable[str]]): fields to be queried - for representations. + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + workfiles. Returns: - Optional[RepresentationDict]: Queried representation - entity or None. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_representation_by_id( + return con.get_workfile_info( project_name=project_name, - representation_id=representation_id, + task_id=task_id, + path=path, fields=fields, own_attributes=own_attributes, ) -def get_representation_by_name( +def get_workfile_info_by_id( project_name: str, - representation_name: str, - version_id: str, + workfile_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity by name and version id. +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by id. Args: - project_name (str): Project where to look for representation. - representation_name (str): Representation name. - version_id (str): Version id. - fields (Optional[Iterable[str]]): fields to be queried - for representations. + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + workfiles. Returns: - Optional[RepresentationDict]: Queried representation entity - or None. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_representation_by_name( + return con.get_workfile_info_by_id( project_name=project_name, - representation_name=representation_name, - version_id=version_id, + workfile_id=workfile_id, fields=fields, own_attributes=own_attributes, ) -def get_representations_hierarchy( +def get_thumbnail_by_id( project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Dict[str, RepresentationHierarchy]: - """Find representation with parents by representation id. + thumbnail_id: str, +) -> ThumbnailContent: + """Get thumbnail from server by id. - Representation entity with parent entities up to project. + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. - Default fields are used when any fields are set to `None`. But it is - possible to pass in empty iterable (list, set, tuple) to skip - entity. + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. Returns: - dict[str, RepresentationHierarchy]: Parent entities by - representation id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representations_hierarchy( + return con.get_thumbnail_by_id( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, + thumbnail_id=thumbnail_id, ) -def get_representation_hierarchy( +def get_thumbnail( project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Optional[RepresentationHierarchy]: - """Find representation parents by representation id. + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Get thumbnail from server. - Representation parent entities up to project. + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. Returns: - RepresentationHierarchy: Representation hierarchy entities. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representation_hierarchy( + return con.get_thumbnail( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, + entity_type=entity_type, + entity_id=entity_id, + thumbnail_id=thumbnail_id, ) -def get_representations_parents( +def get_folder_thumbnail( project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Dict[str, RepresentationParents]: - """Find representations parents by representation id. - - Representation parent entities up to project. + folder_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representations_parents( + return con.get_folder_thumbnail( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + folder_id=folder_id, + thumbnail_id=thumbnail_id, ) -def get_representation_parents( +def get_task_thumbnail( project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Optional["RepresentationParents"]: - """Find representation parents by representation id. - - Representation parent entities up to project. + task_id: str, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. Returns: - RepresentationParents: Representation parent entities. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representation_parents( + return con.get_task_thumbnail( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + task_id=task_id, ) -def get_repre_ids_by_context_filters( +def get_version_thumbnail( project_name: str, - context_filters: Optional[Dict[str, Iterable[str]]], - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, -) -> List[str]: - """Find representation ids which match passed context filters. - - Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or - product name, product type and many more. This implementation gives - option to quickly filter representation based on representation data - in database. - - Context filters have defined structure. To define filter of nested - subfield use dot '.' as delimiter (For example 'task.name'). - Filter values can be regex filters. String or ``re.Pattern`` can - be used. + version_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. Args: - project_name (str): Project where to look for representations. - context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Optional[Iterable[str]]): Representation - names, can be used as additional filter for representations - by their names. - version_ids (Optional[Iterable[str]]): Version ids, can be used - as additional filter for representations by their parent ids. + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - list[str]: Representation ids that match passed filters. - - Example: - The function returns just representation ids so if entities are - required for funtionality they must be queried afterwards by - their ids. - >>> project_name = "testProject" - >>> filters = { - ... "task.name": ["[aA]nimation"], - ... "product": [".*[Mm]ain"] - ... } - >>> repre_ids = get_repre_ids_by_context_filters( - ... project_name, filters) - >>> repres = get_representations(project_name, repre_ids) + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_repre_ids_by_context_filters( + return con.get_version_thumbnail( project_name=project_name, - context_filters=context_filters, - representation_names=representation_names, - version_ids=version_ids, + version_id=version_id, + thumbnail_id=thumbnail_id, ) -def create_representation( +def get_workfile_thumbnail( project_name: str, - name: str, - version_id: str, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - representation_id: Optional[str] = None, -) -> str: - """Create new representation. + workfile_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. Args: - project_name (str): Project name. - name (str): Representation name. - version_id (str): Parent version id. - files (Optional[list[dict]]): Representation files information. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - representation_id (Optional[str]): Representation id. If not - passed new id is generated. + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - str: Representation id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.create_representation( + return con.get_workfile_thumbnail( project_name=project_name, - name=name, - version_id=version_id, - files=files, - attrib=attrib, - data=data, - traits=traits, - tags=tags, - status=status, - active=active, - representation_id=representation_id, + workfile_id=workfile_id, + thumbnail_id=thumbnail_id, ) -def update_representation( +def create_thumbnail( project_name: str, - representation_id: str, - name: Optional[str] = None, - version_id: Optional[str] = None, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, -): - """Update representation entity on server. + src_filepath: str, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. - Update of ``data`` will override existing value on folder entity. + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + Returns: + str: Created thumbnail id. - Args: - project_name (str): Project name. - representation_id (str): Representation id. - name (Optional[str]): New name. - version_id (Optional[str]): New version id. - files (Optional[list[dict]]): New files - information. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - traits (Optional[dict[str, Any]]): New traits. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.update_representation( + return con.create_thumbnail( project_name=project_name, - representation_id=representation_id, - name=name, - version_id=version_id, - files=files, - attrib=attrib, - data=data, - traits=traits, - tags=tags, - status=status, - active=active, + src_filepath=src_filepath, + thumbnail_id=thumbnail_id, ) -def delete_representation( +def update_thumbnail( project_name: str, - representation_id: str, + thumbnail_id: str, + src_filepath: str, ): - """Delete representation. + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. Args: - project_name (str): Project name. - representation_id (str): Representation id to delete. + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.delete_representation( + return con.update_thumbnail( project_name=project_name, - representation_id=representation_id, + thumbnail_id=thumbnail_id, + src_filepath=src_filepath, ) -def get_workfiles_info( +def send_batch_operations( project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - paths: Optional[Iterable[str]] = None, - path_regex: Optional[str] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["WorkfileInfoDict", None, None]: - """Workfile info entities by passed filters. + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """Post multiple CRUD 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): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used - for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used - for filtering. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + 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: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_workfiles_info( + return con.send_batch_operations( project_name=project_name, - workfile_ids=workfile_ids, - task_ids=task_ids, - paths=paths, - path_regex=path_regex, - statuses=statuses, - tags=tags, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_workfile_info( +def send_activities_batch_operations( project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """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): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + 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: - Optional[WorkfileInfoDict]: Workfile info entity or None. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_workfile_info( + return con.send_activities_batch_operations( project_name=project_name, - task_id=task_id, - path=path, - fields=fields, - own_attributes=own_attributes, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_workfile_info_by_id( - project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> List["ActionManifestDict"]: + """Get actions for a context. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + List[ActionManifestDict]: List of action manifests. """ con = get_server_api_connection() - return con.get_workfile_info_by_id( + return con.get_actions( project_name=project_name, - workfile_id=workfile_id, - fields=fields, - own_attributes=own_attributes, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, ) -def get_thumbnail_by_id( - project_name: str, - thumbnail_id: str, -) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. """ con = get_server_api_connection() - return con.get_thumbnail_by_id( + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - thumbnail_id=thumbnail_id, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_thumbnail( - project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Get thumbnail from server. - - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + ActionConfigResponse: Action configuration data. """ con = get_server_api_connection() - return con.get_thumbnail( + return con.get_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, entity_type=entity_type, - entity_id=entity_id, - thumbnail_id=thumbnail_id, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_folder_thumbnail( - project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. +def set_action_config( + identifier: str, + addon_name: str, + addon_version: str, + value: Dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Set action configuration. Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[Dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + ActionConfigResponse: New action configuration data. """ con = get_server_api_connection() - return con.get_folder_thumbnail( + return con.set_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + value=value, project_name=project_name, - folder_id=folder_id, - thumbnail_id=thumbnail_id, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_task_thumbnail( - project_name: str, - task_id: str, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. +def take_action( + action_token: str, +) -> "ActionTakeResponse": + """Take action metadata using an action token. Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. + action_token (str): AYON launcher action token. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + ActionTakeResponse: Action metadata describing how to launch + action. """ con = get_server_api_connection() - return con.get_task_thumbnail( - project_name=project_name, - task_id=task_id, + return con.take_action( + action_token=action_token, ) -def get_version_thumbnail( - project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. +def abort_action( + action_token: str, + message: Optional[str] = None, +) -> None: + """Abort action using an action token. Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. """ con = get_server_api_connection() - return con.get_version_thumbnail( - project_name=project_name, - version_id=version_id, - thumbnail_id=thumbnail_id, + return con.abort_action( + action_token=action_token, + message=message, ) -def get_workfile_thumbnail( - project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. +def get_addon_endpoint( + addon_name: str, + addon_version: str, + *subpaths, +) -> str: + """Calculate endpoint to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + str: Final url. """ con = get_server_api_connection() - return con.get_workfile_thumbnail( - project_name=project_name, - workfile_id=workfile_id, - thumbnail_id=thumbnail_id, + return con.get_addon_endpoint( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, ) -def create_thumbnail( - project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, -) -> str: - """Create new thumbnail on server from passed path. +def get_addons_info( + details: bool = True, +) -> "AddonsInfoDict": + """Get information about addons available on server. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. - - Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. + details (Optional[bool]): Detailed data with information how + to get client code. """ con = get_server_api_connection() - return con.create_thumbnail( - project_name=project_name, - src_filepath=src_filepath, - thumbnail_id=thumbnail_id, + return con.get_addons_info( + details=details, ) -def update_thumbnail( - project_name: str, - thumbnail_id: str, - src_filepath: str, -): - """Change thumbnail content by id. +def get_addon_url( + addon_name: str, + addon_version: str, + *subpaths, + use_rest: bool = True, +) -> str: + """Calculate url to addon route. - Update can be also used to create new thumbnail. + Examples: + + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. - Raises: - ValueError: When thumbnail source cannot be processed. + Returns: + str: Final url. """ con = get_server_api_connection() - return con.update_thumbnail( - project_name=project_name, - thumbnail_id=thumbnail_id, - src_filepath=src_filepath, + return con.get_addon_url( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + use_rest=use_rest, ) -def send_batch_operations( - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD operations to server. +def delete_addon( + addon_name: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon from 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. + Delete all versions of addon from server. 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'. + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. - 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. + """ + con = get_server_api_connection() + return con.delete_addon( + addon_name=addon_name, + purge=purge, + ) - Returns: - list[dict[str, Any]]: Operations result with process details. + +def delete_addon_version( + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon version from server. + + Delete all versions of addon from server. + + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.send_batch_operations( - project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + return con.delete_addon_version( + addon_name=addon_name, + addon_version=addon_version, + purge=purge, ) -def send_activities_batch_operations( - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD activities operations to server. +def upload_addon_zip( + src_filepath: str, + progress: Optional[TransferProgress] = None, +): + """Upload addon zip file 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. + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). - 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'. + Example output:: - 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. + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + + Args: + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. Returns: - list[dict[str, Any]]: Operations result with process details. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.send_activities_batch_operations( - project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + return con.upload_addon_zip( + src_filepath=src_filepath, + progress=progress, ) -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. +def download_addon_private_file( + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. Returns: - List[ActionManifestDict]: List of action manifests. + str: Filepath to downloaded file. """ con = get_server_api_connection() - return con.get_actions( - project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - mode=mode, + return con.download_addon_private_file( + addon_name=addon_name, + addon_version=addon_version, + filename=filename, + destination_dir=destination_dir, + destination_filename=destination_filename, + chunk_size=chunk_size, + progress=progress, ) -def trigger_action( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. - - Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - - """ +def get_rest_folder( + project_name: str, + folder_id: str, +) -> Optional["FolderDict"]: con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.get_rest_folder( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + folder_id=folder_id, ) -def get_action_config( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. +def get_rest_folders( + project_name: str, + include_attrib: bool = False, +) -> list["FlatFolderDict"]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. Returns: - ActionConfigResponse: Action configuration data. + List[FlatFolderDict]: List of folder entities. """ con = get_server_api_connection() - return con.get_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.get_rest_folders( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + include_attrib=include_attrib, ) -def set_action_config( - identifier: str, - addon_name: str, - addon_version: str, - value: Dict[str, Any], - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Set action configuration. +def get_folders_hierarchy( + project_name: str, + search_string: Optional[str] = None, + folder_types: Optional[Iterable[str]] = None, +) -> "ProjectHierarchyDict": + """Get project hierarchy. + + All folders in project in hierarchy data structure. + + Example output: + { + "hierarchy": [ + { + "id": "...", + "name": "...", + "label": "...", + "status": "...", + "folderType": "...", + "hasTasks": False, + "taskNames": [], + "parents": [], + "parentId": None, + "children": [...children folders...] + }, + ... + ] + } Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action - configuration. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where to look for folders. + search_string (Optional[str]): Search string to filter folders. + folder_types (Optional[Iterable[str]]): Folder types to filter. Returns: - ActionConfigResponse: New action configuration data. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.set_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - value=value, + return con.get_folders_hierarchy( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + search_string=search_string, + folder_types=folder_types, ) -def take_action( - action_token: str, -) -> "ActionTakeResponse": - """Take action metadata using an action token. +def get_folders_rest( + project_name: str, + include_attrib: bool = False, +) -> list["FlatFolderDict"]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] + + Deprecated: + Use 'get_rest_folders' instead. Function was renamed to match + other rest functions, like 'get_rest_folder', + 'get_rest_project' etc. . + Will be removed in '1.0.7' or '1.1.0'. Args: - action_token (str): AYON launcher action token. + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. Returns: - ActionTakeResponse: Action metadata describing how to launch - action. + List[FlatFolderDict]: List of folder entities. """ con = get_server_api_connection() - return con.take_action( - action_token=action_token, + return con.get_folders_rest( + project_name=project_name, + include_attrib=include_attrib, ) -def abort_action( - action_token: str, - message: Optional[str] = None, -) -> None: - """Abort action using an action token. +def get_folders( + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + folder_paths: Optional[Iterable[str]] = None, + folder_names: Optional[Iterable[str]] = None, + folder_types: Optional[Iterable[str]] = None, + parent_ids: Optional[Iterable[str]] = None, + folder_path_regex: Optional[str] = None, + has_products: Optional[bool] = None, + has_tasks: Optional[bool] = None, + has_children: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Generator["FolderDict", None, None]: + """Query folders from server. + + Todos: + Folder name won't be unique identifier, so we should add + folder path filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. Args: - action_token (str): AYON launcher action token. - message (Optional[str]): Message to display in the UI. + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Folder ids to filter. + folder_paths (Optional[Iterable[str]]): Folder paths used + for filtering. + folder_names (Optional[Iterable[str]]): Folder names used + for filtering. + folder_types (Optional[Iterable[str]]): Folder types used + for filtering. + parent_ids (Optional[Iterable[str]]): Ids of folder parents. + Use 'None' if folder is direct child of project. + folder_path_regex (Optional[str]): Folder path regex used + for filtering. + has_products (Optional[bool]): Filter folders with/without + products. Ignored when None, default behavior. + has_tasks (Optional[bool]): Filter folders with/without + tasks. Ignored when None, default behavior. + has_children (Optional[bool]): Filter folders with/without + children. Ignored when None, default behavior. + statuses (Optional[Iterable[str]]): Folder statuses used + for filtering. + assignees_all (Optional[Iterable[str]]): Filter by assigness + on children tasks. Task must have all of passed assignees. + tags (Optional[Iterable[str]]): Folder tags used + for filtering. + active (Optional[bool]): Filter active/inactive folders. + Both are returned if is set to None. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[FolderDict, None, None]: Queried folder entities. """ con = get_server_api_connection() - return con.abort_action( - action_token=action_token, - message=message, + return con.get_folders( + project_name=project_name, + folder_ids=folder_ids, + folder_paths=folder_paths, + folder_names=folder_names, + folder_types=folder_types, + parent_ids=parent_ids, + folder_path_regex=folder_path_regex, + has_products=has_products, + has_tasks=has_tasks, + has_children=has_children, + statuses=statuses, + assignees_all=assignees_all, + tags=tags, + active=active, + has_links=has_links, + fields=fields, + own_attributes=own_attributes, ) -def get_addon_endpoint( - addon_name: str, - addon_version: str, - *subpaths, -) -> str: - """Calculate endpoint to addon route. - - Examples: - >>> from ayon_api import ServerAPI - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' +def get_folder_by_id( + project_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["FolderDict"]: + """Query folder entity by id. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - str: Final url. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_addon_endpoint( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, + return con.get_folder_by_id( + project_name=project_name, + folder_id=folder_id, + fields=fields, + own_attributes=own_attributes, ) -def get_addons_info( - details: bool = True, -) -> "AddonsInfoDict": - """Get information about addons available on server. +def get_folder_by_path( + project_name: str, + folder_path: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["FolderDict"]: + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_path (str): Folder path. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. - Args: - details (Optional[bool]): Detailed data with information how - to get client code. + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_addons_info( - details=details, + return con.get_folder_by_path( + project_name=project_name, + folder_path=folder_path, + fields=fields, + own_attributes=own_attributes, ) -def get_addon_url( - addon_name: str, - addon_version: str, - *subpaths, - use_rest: bool = True, -) -> str: - """Calculate url to addon route. - - Examples: +def get_folder_by_name( + project_name: str, + folder_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["FolderDict"]: + """Query folder entity by path. - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - str: Final url. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_addon_url( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - use_rest=use_rest, + return con.get_folder_by_name( + project_name=project_name, + folder_name=folder_name, + fields=fields, + own_attributes=own_attributes, ) -def delete_addon( - addon_name: str, - purge: Optional[bool] = None, -) -> None: - """Delete addon from server. +def get_folder_ids_with_products( + project_name: str, + folder_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """Find folders which have at least one product. - Delete all versions of addon from server. + Folders that have at least one product should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. + + Returns: + set[str]: Folder ids that have at least one product. """ con = get_server_api_connection() - return con.delete_addon( - addon_name=addon_name, - purge=purge, + return con.get_folder_ids_with_products( + project_name=project_name, + folder_ids=folder_ids, ) -def delete_addon_version( - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, -) -> None: - """Delete addon version from server. - - Delete all versions of addon from server. +def create_folder( + project_name: str, + name: str, + folder_type: Optional[str] = None, + parent_id: Optional[str] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + folder_id: Optional[str] = None, +) -> str: + """Create new folder. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Project name. + name (str): Folder name. + folder_type (Optional[str]): Folder type. + parent_id (Optional[str]): Parent folder id. Parent is project + if is ``None``. + label (Optional[str]): Label of folder. + attrib (Optional[dict[str, Any]]): Folder attributes. + data (Optional[dict[str, Any]]): Folder data. + tags (Optional[Iterable[str]]): Folder tags. + status (Optional[str]): Folder status. + active (Optional[bool]): Folder active state. + thumbnail_id (Optional[str]): Folder thumbnail id. + folder_id (Optional[str]): Folder id. If not passed new id is + generated. + + Returns: + str: Entity id. """ con = get_server_api_connection() - return con.delete_addon_version( - addon_name=addon_name, - addon_version=addon_version, - purge=purge, + return con.create_folder( + project_name=project_name, + name=name, + folder_type=folder_type, + parent_id=parent_id, + label=label, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + folder_id=folder_id, ) -def upload_addon_zip( - src_filepath: str, - progress: Optional[TransferProgress] = None, +def update_folder( + project_name: str, + folder_id: str, + name: Optional[str] = None, + folder_type: Optional[str] = None, + parent_id: Optional[str] = NOT_SET, + label: Optional[str] = NOT_SET, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, ): - """Upload addon zip file to server. + """Update folder entity on server. - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). + Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. - Example output:: + Update of ``data`` will override existing value on folder entity. - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - Returns: - dict[str, Any]: Response data from server. + project_name (str): Project name. + folder_id (str): Folder id. + name (Optional[str]): New name. + folder_type (Optional[str]): New folder type. + parent_id (Optional[str]): New parent folder id. + label (Optional[str]): New label. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.upload_addon_zip( - src_filepath=src_filepath, - progress=progress, + return con.update_folder( + project_name=project_name, + folder_id=folder_id, + name=name, + folder_type=folder_type, + parent_id=parent_id, + label=label, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, ) -def download_addon_private_file( - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. +def delete_folder( + project_name: str, + folder_id: str, + force: bool = False, +): + """Delete folder. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. + project_name (str): Project name. + folder_id (str): Folder id to delete. + force (Optional[bool]): Folder delete folder with all children + folder, products, versions and representations. """ con = get_server_api_connection() - return con.download_addon_private_file( - addon_name=addon_name, - addon_version=addon_version, - filename=filename, - destination_dir=destination_dir, - destination_filename=destination_filename, - chunk_size=chunk_size, - progress=progress, + return con.delete_folder( + project_name=project_name, + folder_id=folder_id, + force=force, ) diff --git a/ayon_api/_folders.py b/ayon_api/_folders.py new file mode 100644 index 000000000..76181c8f9 --- /dev/null +++ b/ayon_api/_folders.py @@ -0,0 +1,637 @@ +from __future__ import annotations + +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ._base import _BaseServerAPI +from .exceptions import UnsupportedServerVersion +from .utils import ( + prepare_query_string, + prepare_list_filters, + fill_own_attribs, + create_entity_id, + NOT_SET, +) +from .graphql_queries import folders_graphql_query + +if typing.TYPE_CHECKING: + from .typing import ( + FolderDict, + FlatFolderDict, + ProjectHierarchyDict, + ) + + +class _FoldersAPI(_BaseServerAPI): + def get_rest_folder( + self, project_name: str, folder_id: str + ) -> Optional["FolderDict"]: + return self.get_rest_entity_by_id( + project_name, "folder", folder_id + ) + + def get_rest_folders( + self, project_name: str, include_attrib: bool = False + ) -> list["FlatFolderDict"]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] + + Args: + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. + + Returns: + List[FlatFolderDict]: List of folder entities. + + """ + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 0, 8): + raise UnsupportedServerVersion( + "Function 'get_folders_rest' is supported" + " for AYON server 1.0.8 and above." + ) + query = prepare_query_string({ + "attrib": "true" if include_attrib else "false" + }) + response = self.get( + f"projects/{project_name}/folders{query}" + ) + response.raise_for_status() + return response.data["folders"] + + def get_folders_hierarchy( + self, + project_name: str, + search_string: Optional[str] = None, + folder_types: Optional[Iterable[str]] = None + ) -> "ProjectHierarchyDict": + """Get project hierarchy. + + All folders in project in hierarchy data structure. + + Example output: + { + "hierarchy": [ + { + "id": "...", + "name": "...", + "label": "...", + "status": "...", + "folderType": "...", + "hasTasks": False, + "taskNames": [], + "parents": [], + "parentId": None, + "children": [...children folders...] + }, + ... + ] + } + + Args: + project_name (str): Project where to look for folders. + search_string (Optional[str]): Search string to filter folders. + folder_types (Optional[Iterable[str]]): Folder types to filter. + + Returns: + dict[str, Any]: Response data from server. + + """ + if folder_types: + folder_types = ",".join(folder_types) + + query = prepare_query_string({ + "search": search_string or None, + "types": folder_types or None, + }) + response = self.get( + f"projects/{project_name}/hierarchy{query}" + ) + response.raise_for_status() + return response.data + + def get_folders_rest( + self, project_name: str, include_attrib: bool = False + ) -> list["FlatFolderDict"]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] + + Deprecated: + Use 'get_rest_folders' instead. Function was renamed to match + other rest functions, like 'get_rest_folder', + 'get_rest_project' etc. . + Will be removed in '1.0.7' or '1.1.0'. + + Args: + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. + + Returns: + List[FlatFolderDict]: List of folder entities. + + """ + warnings.warn( + ( + "DEPRECATION: Used deprecated 'get_folders_rest'," + " use 'get_rest_folders' instead." + ), + DeprecationWarning + ) + return self.get_rest_folders(project_name, include_attrib) + + def get_folders( + self, + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + folder_paths: Optional[Iterable[str]] = None, + folder_names: Optional[Iterable[str]] = None, + folder_types: Optional[Iterable[str]] = None, + parent_ids: Optional[Iterable[str]] = None, + folder_path_regex: Optional[str] = None, + has_products: Optional[bool] = None, + has_tasks: Optional[bool] = None, + has_children: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Generator["FolderDict", None, None]: + """Query folders from server. + + Todos: + Folder name won't be unique identifier, so we should add + folder path filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. + + Args: + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Folder ids to filter. + folder_paths (Optional[Iterable[str]]): Folder paths used + for filtering. + folder_names (Optional[Iterable[str]]): Folder names used + for filtering. + folder_types (Optional[Iterable[str]]): Folder types used + for filtering. + parent_ids (Optional[Iterable[str]]): Ids of folder parents. + Use 'None' if folder is direct child of project. + folder_path_regex (Optional[str]): Folder path regex used + for filtering. + has_products (Optional[bool]): Filter folders with/without + products. Ignored when None, default behavior. + has_tasks (Optional[bool]): Filter folders with/without + tasks. Ignored when None, default behavior. + has_children (Optional[bool]): Filter folders with/without + children. Ignored when None, default behavior. + statuses (Optional[Iterable[str]]): Folder statuses used + for filtering. + assignees_all (Optional[Iterable[str]]): Filter by assigness + on children tasks. Task must have all of passed assignees. + tags (Optional[Iterable[str]]): Folder tags used + for filtering. + active (Optional[bool]): Filter active/inactive folders. + Both are returned if is set to None. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[FolderDict, None, None]: Queried folder entities. + + """ + if not project_name: + return + + filters = { + "projectName": project_name + } + if not prepare_list_filters( + filters, + ("folderIds", folder_ids), + ("folderPaths", folder_paths), + ("folderNames", folder_names), + ("folderTypes", folder_types), + ("folderStatuses", statuses), + ("folderTags", tags), + ("folderAssigneesAll", assignees_all), + ): + return + + for filter_key, filter_value in ( + ("folderPathRegex", folder_path_regex), + ("folderHasProducts", has_products), + ("folderHasTasks", has_tasks), + ("folderHasLinks", has_links), + ("folderHasChildren", has_children), + ): + if filter_value is not None: + filters[filter_key] = filter_value + + if parent_ids is not None: + parent_ids = set(parent_ids) + if not parent_ids: + return + if None in parent_ids: + # Replace 'None' with '"root"' which is used during GraphQl + # query for parent ids filter for folders without folder + # parent + parent_ids.remove(None) + parent_ids.add("root") + + if project_name in parent_ids: + # Replace project name with '"root"' which is used during + # GraphQl query for parent ids filter for folders without + # folder parent + parent_ids.remove(project_name) + parent_ids.add("root") + + filters["parentFolderIds"] = list(parent_ids) + + if not fields: + fields = self.get_default_fields_for_type("folder") + else: + fields = set(fields) + self._prepare_fields("folder", fields) + + if active is not None: + fields.add("active") + + if own_attributes: + fields.add("ownAttrib") + + query = folders_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for folder in parsed_data["project"]["folders"]: + if active is not None and active is not folder["active"]: + continue + + self._convert_entity_data(folder) + + if own_attributes: + fill_own_attribs(folder) + yield folder + + def get_folder_by_id( + self, + project_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional["FolderDict"]: + """Query folder entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. + + """ + folders = self.get_folders( + project_name, + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_path( + self, + project_name: str, + folder_path: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional["FolderDict"]: + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_path (str): Folder path. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. + + """ + folders = self.get_folders( + project_name, + folder_paths=[folder_path], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_by_name( + self, + project_name: str, + folder_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional["FolderDict"]: + """Query folder entity by path. + + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[FolderDict]: Folder entity data or None + if was not found. + + """ + folders = self.get_folders( + project_name, + folder_names=[folder_name], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for folder in folders: + return folder + return None + + def get_folder_ids_with_products( + self, project_name: str, folder_ids: Optional[Iterable[str]] = None + ) -> set[str]: + """Find folders which have at least one product. + + Folders that have at least one product should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. + + Args: + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. + + Returns: + set[str]: Folder ids that have at least one product. + + """ + if folder_ids is not None: + folder_ids = set(folder_ids) + if not folder_ids: + return set() + + query = folders_graphql_query({"id"}) + query.set_variable_value("projectName", project_name) + query.set_variable_value("folderHasProducts", True) + if folder_ids: + query.set_variable_value("folderIds", list(folder_ids)) + + parsed_data = query.query(self) + folders = parsed_data["project"]["folders"] + return { + folder["id"] + for folder in folders + } + + def create_folder( + self, + project_name: str, + name: str, + folder_type: Optional[str] = None, + parent_id: Optional[str] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + folder_id: Optional[str] = None, + ) -> str: + """Create new folder. + + Args: + project_name (str): Project name. + name (str): Folder name. + folder_type (Optional[str]): Folder type. + parent_id (Optional[str]): Parent folder id. Parent is project + if is ``None``. + label (Optional[str]): Label of folder. + attrib (Optional[dict[str, Any]]): Folder attributes. + data (Optional[dict[str, Any]]): Folder data. + tags (Optional[Iterable[str]]): Folder tags. + status (Optional[str]): Folder status. + active (Optional[bool]): Folder active state. + thumbnail_id (Optional[str]): Folder thumbnail id. + folder_id (Optional[str]): Folder id. If not passed new id is + generated. + + Returns: + str: Entity id. + + """ + if not folder_id: + folder_id = create_entity_id() + create_data = { + "id": folder_id, + "name": name, + } + for key, value in ( + ("folderType", folder_type), + ("parentId", parent_id), + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/folders", + **create_data + ) + response.raise_for_status() + return folder_id + + def update_folder( + self, + project_name: str, + folder_id: str, + name: Optional[str] = None, + folder_type: Optional[str] = None, + parent_id: Optional[str] = NOT_SET, + label: Optional[str] = NOT_SET, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ): + """Update folder entity on server. + + Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + name (Optional[str]): New name. + folder_type (Optional[str]): New folder type. + parent_id (Optional[str]): New parent folder id. + label (Optional[str]): New label. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("folderType", folder_type), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("label", label), + ("parentId", parent_id), + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/folders/{folder_id}", + **update_data + ) + response.raise_for_status() + + def delete_folder( + self, project_name: str, folder_id: str, force: bool = False + ): + """Delete folder. + + Args: + project_name (str): Project name. + folder_id (str): Folder id to delete. + force (Optional[bool]): Folder delete folder with all children + folder, products, versions and representations. + + """ + url = f"projects/{project_name}/folders/{folder_id}" + if force: + url += "?force=true" + response = self.delete(url) + response.raise_for_status() \ No newline at end of file diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a7f378aa2..8164b0d38 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -81,7 +81,6 @@ ServerNotReached, ServerError, HTTPRequestError, - UnsupportedServerVersion, ) from .utils import ( RequestType, @@ -108,6 +107,7 @@ ) from ._actions import _ActionsAPI from ._addons import _AddonsAPI +from ._folders import _FoldersAPI from ._links import _LinksAPI from ._lists import _ListsAPI from ._projects import _ProjectsAPI @@ -118,7 +118,6 @@ ServerVersion, ActivityType, ActivityReferenceType, - LinkDirection, EventFilter, AttributeScope, AttributeSchemaDataDict, @@ -132,7 +131,6 @@ SecretDict, AnyEntityDict, - ProjectDict, FolderDict, TaskDict, ProductDict, @@ -159,29 +157,6 @@ ) -def _convert_list_filter_value(value): - if value is None: - return None - - if isinstance(value, PatternType): - return [value.pattern] - - if isinstance(value, (int, float, str, bool)): - return [value] - return list(set(value)) - - -def _prepare_list_filters(output, *args, **kwargs): - for key, value in itertools.chain(args, kwargs.items()): - value = _convert_list_filter_value(value) - if value is None: - continue - if not value: - return False - output[key] = value - return True - - def _get_description(response): if HTTPStatus is None: return str(response.orig_response) @@ -398,6 +373,7 @@ def as_user(self, username): class ServerAPI( _ActionsAPI, _AddonsAPI, + _FoldersAPI, _LinksAPI, _ListsAPI, _ProjectsAPI, @@ -4200,71 +4176,6 @@ def get_rest_entity_by_id( return response.data return None - def get_rest_folder( - self, project_name: str, folder_id: str - ) -> Optional["FolderDict"]: - return self.get_rest_entity_by_id( - project_name, "folder", folder_id - ) - - def get_rest_folders( - self, project_name: str, include_attrib: bool = False - ) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] - - Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. - - Returns: - List[FlatFolderDict]: List of folder entities. - - """ - major, minor, patch, _, _ = self.server_version_tuple - if (major, minor, patch) < (1, 0, 8): - raise UnsupportedServerVersion( - "Function 'get_folders_rest' is supported" - " for AYON server 1.0.8 and above." - ) - query = prepare_query_string({ - "attrib": "true" if include_attrib else "false" - }) - response = self.get( - f"projects/{project_name}/folders{query}" - ) - response.raise_for_status() - return response.data["folders"] - def get_rest_task( self, project_name: str, task_id: str ) -> Optional["TaskDict"]: @@ -4287,553 +4198,6 @@ def get_rest_representation( project_name, "representation", representation_id ) - def get_folders_hierarchy( - self, - project_name: str, - search_string: Optional[str] = None, - folder_types: Optional[Iterable[str]] = None - ) -> "ProjectHierarchyDict": - """Get project hierarchy. - - All folders in project in hierarchy data structure. - - Example output: - { - "hierarchy": [ - { - "id": "...", - "name": "...", - "label": "...", - "status": "...", - "folderType": "...", - "hasTasks": False, - "taskNames": [], - "parents": [], - "parentId": None, - "children": [...children folders...] - }, - ... - ] - } - - Args: - project_name (str): Project where to look for folders. - search_string (Optional[str]): Search string to filter folders. - folder_types (Optional[Iterable[str]]): Folder types to filter. - - Returns: - dict[str, Any]: Response data from server. - - """ - if folder_types: - folder_types = ",".join(folder_types) - - query = prepare_query_string({ - "search": search_string or None, - "types": folder_types or None, - }) - response = self.get( - f"projects/{project_name}/hierarchy{query}" - ) - response.raise_for_status() - return response.data - - def get_folders_rest( - self, project_name: str, include_attrib: bool = False - ) -> List["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] - - Deprecated: - Use 'get_rest_folders' instead. Function was renamed to match - other rest functions, like 'get_rest_folder', - 'get_rest_project' etc. . - Will be removed in '1.0.7' or '1.1.0'. - - Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. - - Returns: - List[FlatFolderDict]: List of folder entities. - - """ - warnings.warn( - ( - "DEPRECATION: Used deprecated 'get_folders_rest'," - " use 'get_rest_folders' instead." - ), - DeprecationWarning - ) - return self.get_rest_folders(project_name, include_attrib) - - def get_folders( - self, - project_name: str, - folder_ids: Optional[Iterable[str]] = None, - folder_paths: Optional[Iterable[str]] = None, - folder_names: Optional[Iterable[str]] = None, - folder_types: Optional[Iterable[str]] = None, - parent_ids: Optional[Iterable[str]] = None, - folder_path_regex: Optional[str] = None, - has_products: Optional[bool] = None, - has_tasks: Optional[bool] = None, - has_children: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Generator["FolderDict", None, None]: - """Query folders from server. - - Todos: - Folder name won't be unique identifier, so we should add - folder path filtering. - - Notes: - Filter 'active' don't have direct filter in GraphQl. - - Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Folder ids to filter. - folder_paths (Optional[Iterable[str]]): Folder paths used - for filtering. - folder_names (Optional[Iterable[str]]): Folder names used - for filtering. - folder_types (Optional[Iterable[str]]): Folder types used - for filtering. - parent_ids (Optional[Iterable[str]]): Ids of folder parents. - Use 'None' if folder is direct child of project. - folder_path_regex (Optional[str]): Folder path regex used - for filtering. - has_products (Optional[bool]): Filter folders with/without - products. Ignored when None, default behavior. - has_tasks (Optional[bool]): Filter folders with/without - tasks. Ignored when None, default behavior. - has_children (Optional[bool]): Filter folders with/without - children. Ignored when None, default behavior. - statuses (Optional[Iterable[str]]): Folder statuses used - for filtering. - assignees_all (Optional[Iterable[str]]): Filter by assigness - on children tasks. Task must have all of passed assignees. - tags (Optional[Iterable[str]]): Folder tags used - for filtering. - active (Optional[bool]): Filter active/inactive folders. - Both are returned if is set to None. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[FolderDict, None, None]: Queried folder entities. - - """ - if not project_name: - return - - filters = { - "projectName": project_name - } - if not _prepare_list_filters( - filters, - ("folderIds", folder_ids), - ("folderPaths", folder_paths), - ("folderNames", folder_names), - ("folderTypes", folder_types), - ("folderStatuses", statuses), - ("folderTags", tags), - ("folderAssigneesAll", assignees_all), - ): - return - - for filter_key, filter_value in ( - ("folderPathRegex", folder_path_regex), - ("folderHasProducts", has_products), - ("folderHasTasks", has_tasks), - ("folderHasLinks", has_links), - ("folderHasChildren", has_children), - ): - if filter_value is not None: - filters[filter_key] = filter_value - - if parent_ids is not None: - parent_ids = set(parent_ids) - if not parent_ids: - return - if None in parent_ids: - # Replace 'None' with '"root"' which is used during GraphQl - # query for parent ids filter for folders without folder - # parent - parent_ids.remove(None) - parent_ids.add("root") - - if project_name in parent_ids: - # Replace project name with '"root"' which is used during - # GraphQl query for parent ids filter for folders without - # folder parent - parent_ids.remove(project_name) - parent_ids.add("root") - - filters["parentFolderIds"] = list(parent_ids) - - if not fields: - fields = self.get_default_fields_for_type("folder") - else: - fields = set(fields) - self._prepare_fields("folder", fields) - - if active is not None: - fields.add("active") - - if own_attributes: - fields.add("ownAttrib") - - query = folders_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for folder in parsed_data["project"]["folders"]: - if active is not None and active is not folder["active"]: - continue - - self._convert_entity_data(folder) - - if own_attributes: - fill_own_attribs(folder) - yield folder - - def get_folder_by_id( - self, - project_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["FolderDict"]: - """Query folder entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. - - """ - folders = self.get_folders( - project_name, - folder_ids=[folder_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_by_path( - self, - project_name: str, - folder_path: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["FolderDict"]: - """Query folder entity by path. - - Folder path is a path to folder with all parent names joined by slash. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_path (str): Folder path. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. - - """ - folders = self.get_folders( - project_name, - folder_paths=[folder_path], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_by_name( - self, - project_name: str, - folder_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["FolderDict"]: - """Query folder entity by path. - - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_name (str): Folder name. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. - - """ - folders = self.get_folders( - project_name, - folder_names=[folder_name], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for folder in folders: - return folder - return None - - def get_folder_ids_with_products( - self, project_name: str, folder_ids: Optional[Iterable[str]] = None - ) -> Set[str]: - """Find folders which have at least one product. - - Folders that have at least one product should be immutable, so they - should not change path -> change of name or name of any parent - is not possible. - - Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Limit folder ids filtering - to a set of folders. If set to None all folders on project are - checked. - - Returns: - set[str]: Folder ids that have at least one product. - - """ - if folder_ids is not None: - folder_ids = set(folder_ids) - if not folder_ids: - return set() - - query = folders_graphql_query({"id"}) - query.set_variable_value("projectName", project_name) - query.set_variable_value("folderHasProducts", True) - if folder_ids: - query.set_variable_value("folderIds", list(folder_ids)) - - parsed_data = query.query(self) - folders = parsed_data["project"]["folders"] - return { - folder["id"] - for folder in folders - } - - def create_folder( - self, - project_name: str, - name: str, - folder_type: Optional[str] = None, - parent_id: Optional[str] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - folder_id: Optional[str] = None, - ) -> str: - """Create new folder. - - Args: - project_name (str): Project name. - name (str): Folder name. - folder_type (Optional[str]): Folder type. - parent_id (Optional[str]): Parent folder id. Parent is project - if is ``None``. - label (Optional[str]): Label of folder. - attrib (Optional[dict[str, Any]]): Folder attributes. - data (Optional[dict[str, Any]]): Folder data. - tags (Optional[Iterable[str]]): Folder tags. - status (Optional[str]): Folder status. - active (Optional[bool]): Folder active state. - thumbnail_id (Optional[str]): Folder thumbnail id. - folder_id (Optional[str]): Folder id. If not passed new id is - generated. - - Returns: - str: Entity id. - - """ - if not folder_id: - folder_id = create_entity_id() - create_data = { - "id": folder_id, - "name": name, - } - for key, value in ( - ("folderType", folder_type), - ("parentId", parent_id), - ("label", label), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ("thumbnailId", thumbnail_id), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/folders", - **create_data - ) - response.raise_for_status() - return folder_id - - def update_folder( - self, - project_name: str, - folder_id: str, - name: Optional[str] = None, - folder_type: Optional[str] = None, - parent_id: Optional[str] = NOT_SET, - label: Optional[str] = NOT_SET, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, - ): - """Update folder entity on server. - - Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - folder_id (str): Folder id. - name (Optional[str]): New name. - folder_type (Optional[str]): New folder type. - parent_id (Optional[Union[str, None]]): New parent folder id. - label (Optional[Union[str, None]]): New label. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("folderType", folder_type), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - for key, value in ( - ("label", label), - ("parentId", parent_id), - ("thumbnailId", thumbnail_id), - ): - if value is not NOT_SET: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/folders/{folder_id}", - **update_data - ) - response.raise_for_status() - - def delete_folder( - self, project_name: str, folder_id: str, force: bool = False - ): - """Delete folder. - - Args: - project_name (str): Project name. - folder_id (str): Folder id to delete. - force (Optional[bool]): Folder delete folder with all children - folder, products, versions and representations. - - """ - url = f"projects/{project_name}/folders/{folder_id}" - if force: - url += "?force=true" - response = self.delete(url) - response.raise_for_status() - def get_tasks( self, project_name: str, From f02e25cd8f8179ac464c766cc92976658197f8e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:05:03 +0200 Subject: [PATCH 132/506] moved events api to separate file --- automated_api.py | 2 + ayon_api/__init__.py | 24 +- ayon_api/_api.py | 790 ++++++++++++++++++++--------------------- ayon_api/_events.py | 381 ++++++++++++++++++++ ayon_api/server_api.py | 648 +-------------------------------- 5 files changed, 792 insertions(+), 1053 deletions(-) create mode 100644 ayon_api/_events.py diff --git a/automated_api.py b/automated_api.py index 64ec6bd92..f630ed123 100644 --- a/automated_api.py +++ b/automated_api.py @@ -337,6 +337,7 @@ def prepare_api_functions(api_globals): ServerAPI, _ActionsAPI, _AddonsAPI, + _EventsAPI, _FoldersAPI, _LinksAPI, _ListsAPI, @@ -347,6 +348,7 @@ def prepare_api_functions(api_globals): _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) + _items.extend(_EventsAPI.__dict__.items()) _items.extend(_FoldersAPI.__dict__.items()) _items.extend(_LinksAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 11bf7df5b..66379e097 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -67,12 +67,6 @@ patch, get, delete, - get_event, - get_events, - update_event, - dispatch_event, - delete_event, - enroll_event_job, get_activities, get_activity_by_id, create_activity, @@ -210,6 +204,12 @@ delete_addon_version, upload_addon_zip, download_addon_private_file, + get_event, + get_events, + update_event, + dispatch_event, + delete_event, + enroll_event_job, get_rest_folder, get_rest_folders, get_folders_hierarchy, @@ -331,12 +331,6 @@ "patch", "get", "delete", - "get_event", - "get_events", - "update_event", - "dispatch_event", - "delete_event", - "enroll_event_job", "get_activities", "get_activity_by_id", "create_activity", @@ -474,6 +468,12 @@ "delete_addon_version", "upload_addon_zip", "download_addon_private_file", + "get_event", + "get_events", + "update_event", + "dispatch_event", + "delete_event", + "enroll_event_job", "get_rest_folder", "get_rest_folders", "get_folders_hierarchy", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index db11667a1..d6a6f8ba7 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -904,444 +904,155 @@ def delete( ) -def get_event( - event_id: str, -) -> Optional[Dict[str, Any]]: - """Query full event data by id. - - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. - - Args: - event_id (str): Event id. - - Returns: - dict[str, Any]: Full event data. - - """ - con = get_server_api_connection() - return con.get_event( - event_id=event_id, - ) - - -def get_events( - topics: Optional[Iterable[str]] = None, - event_ids: Optional[Iterable[str]] = None, - project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - users: Optional[Iterable[str]] = None, - include_logs: Optional[bool] = None, - has_children: Optional[bool] = None, - newer_than: Optional[str] = None, - older_than: Optional[str] = None, +def get_activities( + project_name: str, + 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["ActivityReferenceType"]] = None, fields: Optional[Iterable[str]] = None, limit: Optional[int] = None, order: Optional[SortOrder] = None, - states: Optional[Iterable[str]] = None, ) -> Generator[Dict[str, Any], None, None]: - """Get events from server with filtering options. - - Notes: - Not all event happen on a project. + """Get activities from server with filtering options. Args: - topics (Optional[Iterable[str]]): Name of topics. - event_ids (Optional[Iterable[str]]): Event ids. - project_names (Optional[Iterable[str]]): Project on which - event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. - users (Optional[Iterable[str]]): Filtering by users - who created/triggered an event. - include_logs (Optional[bool]): Query also log events. - has_children (Optional[bool]): Event is with/without children - events. If 'None' then all events are returned, default. - newer_than (Optional[str]): Return only events newer than given - iso datetime string. - older_than (Optional[str]): Return only events older than given - iso datetime string. + 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[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. fields (Optional[Iterable[str]]): Fields that should be received - for each event. - limit (Optional[int]): Limit number of events to be fetched. - order (Optional[SortOrder]): Order events in ascending + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending or descending order. It is recommended to set 'limit' when used descending. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. Returns: - Generator[dict[str, Any]]: Available events matching filters. + Generator[dict[str, Any]]: Available activities matching filters. """ con = get_server_api_connection() - return con.get_events( - topics=topics, - event_ids=event_ids, - project_names=project_names, - statuses=statuses, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, + return con.get_activities( + project_name=project_name, + activity_ids=activity_ids, + activity_types=activity_types, + entity_ids=entity_ids, + entity_names=entity_names, + entity_type=entity_type, + changed_after=changed_after, + changed_before=changed_before, + reference_types=reference_types, fields=fields, limit=limit, order=order, - states=states, ) -def update_event( - event_id: str, - sender: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - status: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - progress: Optional[int] = None, - retries: Optional[int] = None, -): - """Update event data. +def get_activity_by_id( + 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. Args: - event_id (str): Event id. - sender (Optional[str]): New sender of event. - project_name (Optional[str]): New project name. - username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", - "in_progress", "finished", "failed", "aborted", "restarted" - description (Optional[str]): New description. - summary (Optional[dict[str, Any]]): New summary. - payload (Optional[dict[str, Any]]): New payload. - progress (Optional[int]): New progress. Range [0-100]. - retries (Optional[int]): New retries. + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + 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.update_event( - event_id=event_id, - sender=sender, + return con.get_activity_by_id( project_name=project_name, - username=username, - status=status, - description=description, - summary=summary, - payload=payload, - progress=progress, - retries=retries, + activity_id=activity_id, + reference_types=reference_types, + fields=fields, ) -def dispatch_event( - topic: str, - sender: Optional[str] = None, - event_hash: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - depends_on: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - finished: bool = True, - store: bool = True, - dependencies: Optional[List[str]] = None, -): - """Dispatch event to server. +def create_activity( + 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, +) -> str: + """Create activity on a project. Args: - topic (str): Event topic used for filtering of listeners. - sender (Optional[str]): Sender of event. - event_hash (Optional[str]): Event hash. - project_name (Optional[str]): Project name. - depends_on (Optional[str]): Add dependency to another event. - username (Optional[str]): Username which triggered event. - description (Optional[str]): Description of event. - summary (Optional[dict[str, Any]]): Summary of event that can - be used for simple filtering on listeners. - payload (Optional[dict[str, Any]]): Full payload of event data with - all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible - future processing otherwise is event send only - to active listeners. - dependencies (Optional[list[str]]): Deprecated. - List of event id dependencies. + 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: - RestApiResponse: Response from server. + str: Activity id. """ con = get_server_api_connection() - return con.dispatch_event( - topic=topic, - sender=sender, - event_hash=event_hash, + return con.create_activity( project_name=project_name, - username=username, - depends_on=depends_on, - description=description, - summary=summary, - payload=payload, - finished=finished, - store=store, - dependencies=dependencies, + entity_id=entity_id, + entity_type=entity_type, + activity_type=activity_type, + activity_id=activity_id, + body=body, + file_ids=file_ids, + timestamp=timestamp, + data=data, ) -def delete_event( - event_id: str, +def update_activity( + project_name: str, + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[List[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[Dict[str, Any]] = None, ): - """Delete event by id. - - Supported since AYON server 1.6.0. + """Update activity by id. Args: - event_id (str): Event id. - - Returns: - RestApiResponse: Response from server. - - """ - con = get_server_api_connection() - return con.delete_event( - event_id=event_id, - ) - - -def enroll_event_job( - source_topic: "Union[str, List[str]]", - target_topic: str, - sender: str, - description: Optional[str] = None, - sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, - max_retries: Optional[int] = None, - ignore_older_than: Optional[str] = None, - ignore_sender_types: Optional[str] = None, -): - """Enroll job based on events. - - Enroll will find first unprocessed event with 'source_topic' and will - create new event with 'target_topic' for it and return the new event - data. - - Use 'sequential' to control that only single target event is created - at same time. Creation of new target events is blocked while there is - at least one unfinished event with target topic, when set to 'True'. - This helps when order of events matter and more than one process using - the same target is running at the same time. - - Make sure the new event has updated status to '"finished"' status - when you're done with logic - - Target topic should not clash with other processes/services. - - Created target event have 'dependsOn' key where is id of source topic. - - Use-case: - - Service 1 is creating events with topic 'my.leech' - - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1-n machines - - all events must be processed in a sequence by their creation - time and only one event can be processed at a time - - in this case 'sequential' should be set to 'True' so only - one machine is actually processing events, but if one goes - down there are other that can take place - - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1-n machines - - order of events is not important - - 'sequential' should be 'False' - - Args: - source_topic (Union[str, List[str]]): Source topic to enroll with - wildcards '*', or explicit list of topics. - target_topic (str): Topic of dependent event. - sender (str): Identifier of sender (e.g. service name or username). - description (Optional[str]): Human readable text shown - in target event. - sequential (Optional[bool]): The source topic must be processed - in sequence. - events_filter (Optional[dict[str, Any]]): Filtering conditions - to filter the source event. For more technical specifications - look to server backed 'ayon_server.sqlfilter.Filter'. - TODO: Add example of filters. - max_retries (Optional[int]): How many times can be event retried. - Default value is based on server (3 at the time of this PR). - ignore_older_than (Optional[int]): Ignore events older than - given number in days. - ignore_sender_types (Optional[List[str]]): Ignore events triggered - by given sender types. - - Returns: - Union[None, dict[str, Any]]: None if there is no event matching - filters. Created event with 'target_topic'. - - """ - con = get_server_api_connection() - return con.enroll_event_job( - source_topic=source_topic, - target_topic=target_topic, - sender=sender, - description=description, - sequential=sequential, - events_filter=events_filter, - max_retries=max_retries, - ignore_older_than=ignore_older_than, - ignore_sender_types=ignore_sender_types, - ) - - -def get_activities( - project_name: str, - 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["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, -) -> Generator[Dict[str, Any], None, None]: - """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[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. - - Returns: - Generator[dict[str, Any]]: Available activities matching filters. - - """ - con = get_server_api_connection() - return con.get_activities( - project_name=project_name, - activity_ids=activity_ids, - activity_types=activity_types, - entity_ids=entity_ids, - entity_names=entity_names, - entity_type=entity_type, - changed_after=changed_after, - changed_before=changed_before, - reference_types=reference_types, - fields=fields, - limit=limit, - order=order, - ) - - -def get_activity_by_id( - 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. - - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - 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( - project_name=project_name, - activity_id=activity_id, - reference_types=reference_types, - fields=fields, - ) - - -def create_activity( - 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, -) -> str: - """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: - str: Activity id. - - """ - con = get_server_api_connection() - return con.create_activity( - project_name=project_name, - entity_id=entity_id, - entity_type=entity_type, - activity_type=activity_type, - activity_id=activity_id, - body=body, - file_ids=file_ids, - timestamp=timestamp, - data=data, - ) - - -def update_activity( - project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[Dict[str, Any]] = 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. - append_file_ids (Optional[bool]): Append file ids to existing - list of file ids. - data (Optional[Dict[str, Any]]): Update data in activity. + 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. + 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() @@ -5608,6 +5319,295 @@ def download_addon_private_file( ) +def get_event( + event_id: str, +) -> Optional[dict[str, Any]]: + """Query full event data by id. + + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. + + Args: + event_id (str): Event id. + + Returns: + dict[str, Any]: Full event data. + + """ + con = get_server_api_connection() + return con.get_event( + event_id=event_id, + ) + + +def get_events( + topics: Optional[Iterable[str]] = None, + event_ids: Optional[Iterable[str]] = None, + project_names: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + users: Optional[Iterable[str]] = None, + include_logs: Optional[bool] = None, + has_children: Optional[bool] = None, + newer_than: Optional[str] = None, + older_than: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + states: Optional[Iterable[str]] = None, +) -> Generator[dict[str, Any], None, None]: + """Get events from server with filtering options. + + Notes: + Not all event happen on a project. + + Args: + topics (Optional[Iterable[str]]): Name of topics. + event_ids (Optional[Iterable[str]]): Event ids. + project_names (Optional[Iterable[str]]): Project on which + event happened. + statuses (Optional[Iterable[str]]): Filtering by statuses. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + has_children (Optional[bool]): Event is with/without children + events. If 'None' then all events are returned, default. + newer_than (Optional[str]): Return only events newer than given + iso datetime string. + older_than (Optional[str]): Return only events older than given + iso datetime string. + fields (Optional[Iterable[str]]): Fields that should be received + for each event. + limit (Optional[int]): Limit number of events to be fetched. + order (Optional[SortOrder]): Order events in ascending + or descending order. It is recommended to set 'limit' + when used descending. + states (Optional[Iterable[str]]): DEPRECATED Filtering by states. + Use 'statuses' instead. + + Returns: + Generator[dict[str, Any]]: Available events matching filters. + + """ + con = get_server_api_connection() + return con.get_events( + topics=topics, + event_ids=event_ids, + project_names=project_names, + statuses=statuses, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields, + limit=limit, + order=order, + states=states, + ) + + +def update_event( + event_id: str, + sender: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + status: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + progress: Optional[int] = None, + retries: Optional[int] = None, +): + """Update event data. + + Args: + event_id (str): Event id. + sender (Optional[str]): New sender of event. + project_name (Optional[str]): New project name. + username (Optional[str]): New username. + status (Optional[str]): New event status. Enum: "pending", + "in_progress", "finished", "failed", "aborted", "restarted" + description (Optional[str]): New description. + summary (Optional[dict[str, Any]]): New summary. + payload (Optional[dict[str, Any]]): New payload. + progress (Optional[int]): New progress. Range [0-100]. + retries (Optional[int]): New retries. + + """ + con = get_server_api_connection() + return con.update_event( + event_id=event_id, + sender=sender, + project_name=project_name, + username=username, + status=status, + description=description, + summary=summary, + payload=payload, + progress=progress, + retries=retries, + ) + + +def dispatch_event( + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, +): + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (Optional[bool]): Mark event as finished on dispatch. + store (Optional[bool]): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + RestApiResponse: Response from server. + + """ + con = get_server_api_connection() + return con.dispatch_event( + topic=topic, + sender=sender, + event_hash=event_hash, + project_name=project_name, + username=username, + depends_on=depends_on, + description=description, + summary=summary, + payload=payload, + finished=finished, + store=store, + dependencies=dependencies, + ) + + +def delete_event( + event_id: str, +): + """Delete event by id. + + Supported since AYON server 1.6.0. + + Args: + event_id (str): Event id. + + Returns: + RestApiResponse: Response from server. + + """ + con = get_server_api_connection() + return con.delete_event( + event_id=event_id, + ) + + +def enroll_event_job( + source_topic: "Union[str, list[str]]", + target_topic: str, + sender: str, + description: Optional[str] = None, + sequential: Optional[bool] = None, + events_filter: Optional["EventFilter"] = None, + max_retries: Optional[int] = None, + ignore_older_than: Optional[str] = None, + ignore_sender_types: Optional[str] = None, +): + """Enroll job based on events. + + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. + + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. + + Make sure the new event has updated status to '"finished"' status + when you're done with logic + + Target topic should not clash with other processes/services. + + Created target event have 'dependsOn' key where is id of source topic. + + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1-n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1-n machines + - order of events is not important + - 'sequential' should be 'False' + + Args: + source_topic (Union[str, list[str]]): Source topic to enroll with + wildcards '*', or explicit list of topics. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). + ignore_older_than (Optional[int]): Ignore events older than + given number in days. + ignore_sender_types (Optional[list[str]]): Ignore events triggered + by given sender types. + + Returns: + Optional[dict[str, Any]]: None if there is no event matching + filters. Created event with 'target_topic'. + + """ + con = get_server_api_connection() + return con.enroll_event_job( + source_topic=source_topic, + target_topic=target_topic, + sender=sender, + description=description, + sequential=sequential, + events_filter=events_filter, + max_retries=max_retries, + ignore_older_than=ignore_older_than, + ignore_sender_types=ignore_sender_types, + ) + + def get_rest_folder( project_name: str, folder_id: str, diff --git a/ayon_api/_events.py b/ayon_api/_events.py new file mode 100644 index 000000000..d91aebb07 --- /dev/null +++ b/ayon_api/_events.py @@ -0,0 +1,381 @@ +import warnings +import typing +from typing import Optional, Any, Iterable, Generator + +from ._base import _BaseServerAPI +from .utils import SortOrder, prepare_list_filters +from .graphql_queries import events_graphql_query + +if typing.TYPE_CHECKING: + from typing import Union + from .typing import EventFilter + + +class _EventsAPI(_BaseServerAPI): + def get_event(self, event_id: str) -> Optional[dict[str, Any]]: + """Query full event data by id. + + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. + + Args: + event_id (str): Event id. + + Returns: + dict[str, Any]: Full event data. + + """ + response = self.get(f"events/{event_id}") + response.raise_for_status() + return response.data + + def get_events( + self, + topics: Optional[Iterable[str]] = None, + event_ids: Optional[Iterable[str]] = None, + project_names: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + users: Optional[Iterable[str]] = None, + include_logs: Optional[bool] = None, + has_children: Optional[bool] = None, + newer_than: Optional[str] = None, + older_than: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + states: Optional[Iterable[str]] = None, + ) -> Generator[dict[str, Any], None, None]: + """Get events from server with filtering options. + + Notes: + Not all event happen on a project. + + Args: + topics (Optional[Iterable[str]]): Name of topics. + event_ids (Optional[Iterable[str]]): Event ids. + project_names (Optional[Iterable[str]]): Project on which + event happened. + statuses (Optional[Iterable[str]]): Filtering by statuses. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + has_children (Optional[bool]): Event is with/without children + events. If 'None' then all events are returned, default. + newer_than (Optional[str]): Return only events newer than given + iso datetime string. + older_than (Optional[str]): Return only events older than given + iso datetime string. + fields (Optional[Iterable[str]]): Fields that should be received + for each event. + limit (Optional[int]): Limit number of events to be fetched. + order (Optional[SortOrder]): Order events in ascending + or descending order. It is recommended to set 'limit' + when used descending. + states (Optional[Iterable[str]]): DEPRECATED Filtering by states. + Use 'statuses' instead. + + Returns: + Generator[dict[str, Any]]: Available events matching filters. + + """ + if statuses is None and states is not None: + warnings.warn( + ( + "Used deprecated argument 'states' in 'get_events'." + " Use 'statuses' instead." + ), + DeprecationWarning + ) + statuses = states + + filters = {} + if not prepare_list_filters( + filters, + ("eventTopics", topics), + ("eventIds", event_ids), + ("projectNames", project_names), + ("eventStatuses", statuses), + ("eventUsers", users), + ): + return + + if include_logs is None: + include_logs = False + + for filter_key, filter_value in ( + ("includeLogsFilter", include_logs), + ("hasChildrenFilter", has_children), + ("newerThanFilter", newer_than), + ("olderThanFilter", older_than), + ): + if filter_value is not None: + filters[filter_key] = filter_value + + if not fields: + fields = self.get_default_fields_for_type("event") + + major, minor, patch, _, _ = self.server_version_tuple + use_states = (major, minor, patch) <= (1, 5, 6) + + query = events_graphql_query(set(fields), order, use_states) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + if limit: + events_field = query.get_field_by_path("events") + events_field.set_limit(limit) + + for parsed_data in query.continuous_query(self): + for event in parsed_data["events"]: + yield event + + def update_event( + self, + event_id: str, + sender: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + status: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + progress: Optional[int] = None, + retries: Optional[int] = None, + ): + """Update event data. + + Args: + event_id (str): Event id. + sender (Optional[str]): New sender of event. + project_name (Optional[str]): New project name. + username (Optional[str]): New username. + status (Optional[str]): New event status. Enum: "pending", + "in_progress", "finished", "failed", "aborted", "restarted" + description (Optional[str]): New description. + summary (Optional[dict[str, Any]]): New summary. + payload (Optional[dict[str, Any]]): New payload. + progress (Optional[int]): New progress. Range [0-100]. + retries (Optional[int]): New retries. + + """ + kwargs = { + key: value + for key, value in ( + ("sender", sender), + ("project", project_name), + ("user", username), + ("status", status), + ("description", description), + ("summary", summary), + ("payload", payload), + ("progress", progress), + ("retries", retries), + ) + if value is not None + } + + response = self.patch( + f"events/{event_id}", + **kwargs + ) + response.raise_for_status() + + def dispatch_event( + self, + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, + ): + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (Optional[bool]): Mark event as finished on dispatch. + store (Optional[bool]): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + RestApiResponse: Response from server. + + """ + if summary is None: + summary = {} + if payload is None: + payload = {} + event_data = { + "topic": topic, + "sender": sender, + "hash": event_hash, + "project": project_name, + "user": username, + "description": description, + "summary": summary, + "payload": payload, + "finished": finished, + "store": store, + } + if depends_on: + event_data["dependsOn"] = depends_on + + if dependencies: + warnings.warn( + ( + "Used deprecated argument 'dependencies' in" + " 'dispatch_event'. Use 'depends_on' instead." + ), + DeprecationWarning + ) + + response = self.post("events", **event_data) + response.raise_for_status() + return response + + def delete_event(self, event_id: str): + """Delete event by id. + + Supported since AYON server 1.6.0. + + Args: + event_id (str): Event id. + + Returns: + RestApiResponse: Response from server. + + """ + response = self.delete(f"events/{event_id}") + response.raise_for_status() + return response + + def enroll_event_job( + self, + source_topic: "Union[str, list[str]]", + target_topic: str, + sender: str, + description: Optional[str] = None, + sequential: Optional[bool] = None, + events_filter: Optional["EventFilter"] = None, + max_retries: Optional[int] = None, + ignore_older_than: Optional[str] = None, + ignore_sender_types: Optional[str] = None, + ): + """Enroll job based on events. + + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. + + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. + + Make sure the new event has updated status to '"finished"' status + when you're done with logic + + Target topic should not clash with other processes/services. + + Created target event have 'dependsOn' key where is id of source topic. + + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1-n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1-n machines + - order of events is not important + - 'sequential' should be 'False' + + Args: + source_topic (Union[str, list[str]]): Source topic to enroll with + wildcards '*', or explicit list of topics. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). + ignore_older_than (Optional[int]): Ignore events older than + given number in days. + ignore_sender_types (Optional[list[str]]): Ignore events triggered + by given sender types. + + Returns: + Optional[dict[str, Any]]: None if there is no event matching + filters. Created event with 'target_topic'. + + """ + kwargs: dict[str, Any] = { + "sourceTopic": source_topic, + "targetTopic": target_topic, + "sender": sender, + } + major, minor, patch, _, _ = self.get_server_version_tuple() + if max_retries is not None: + kwargs["maxRetries"] = max_retries + if sequential is not None: + kwargs["sequential"] = sequential + if description is not None: + kwargs["description"] = description + if events_filter is not None: + kwargs["filter"] = events_filter + if ( + ignore_older_than is not None + and (major, minor, patch) > (1, 5, 1) + ): + kwargs["ignoreOlderThan"] = ignore_older_than + if ignore_sender_types is not None: + if (major, minor, patch) <= (1, 5, 4): + raise ValueError( + "Ignore sender types are not supported for" + f" your version of server {self.get_server_version()}." + ) + kwargs["ignoreSenderTypes"] = list(ignore_sender_types) + + response = self.post("enroll", **kwargs) + if response.status_code == 204: + return None + + if response.status_code == 503: + # Server is busy + self.log.info("Server is busy. Can't enroll event now.") + return None + + if response.status_code >= 400: + self.log.error(response.text) + return None + + return response.data \ No newline at end of file diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 8164b0d38..9dff80799 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -62,7 +62,6 @@ from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( product_types_query, - folders_graphql_query, tasks_graphql_query, tasks_by_folder_paths_graphql_query, products_graphql_query, @@ -70,9 +69,7 @@ representations_graphql_query, representations_hierarchy_qraphql_query, workfiles_info_graphql_query, - events_graphql_query, users_graphql_query, - activities_graphql_query, ) from .exceptions import ( FailedOperations, @@ -107,6 +104,7 @@ ) from ._actions import _ActionsAPI from ._addons import _AddonsAPI +from ._events import _EventsAPI from ._folders import _FoldersAPI from ._links import _LinksAPI from ._lists import _ListsAPI @@ -118,7 +116,6 @@ ServerVersion, ActivityType, ActivityReferenceType, - EventFilter, AttributeScope, AttributeSchemaDataDict, AttributeSchemaDict, @@ -373,6 +370,7 @@ def as_user(self, username): class ServerAPI( _ActionsAPI, _AddonsAPI, + _EventsAPI, _FoldersAPI, _LinksAPI, _ListsAPI, @@ -1484,609 +1482,6 @@ def get(self, entrypoint: str, **kwargs): def delete(self, entrypoint: str, **kwargs): return self.raw_delete(entrypoint, params=kwargs) - def get_event(self, event_id: str) -> Optional[Dict[str, Any]]: - """Query full event data by id. - - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. - - Args: - event_id (str): Event id. - - Returns: - dict[str, Any]: Full event data. - - """ - response = self.get(f"events/{event_id}") - response.raise_for_status() - return response.data - - def get_events( - self, - topics: Optional[Iterable[str]] = None, - event_ids: Optional[Iterable[str]] = None, - project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - users: Optional[Iterable[str]] = None, - include_logs: Optional[bool] = None, - has_children: Optional[bool] = None, - newer_than: Optional[str] = None, - older_than: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - states: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: - """Get events from server with filtering options. - - Notes: - Not all event happen on a project. - - Args: - topics (Optional[Iterable[str]]): Name of topics. - event_ids (Optional[Iterable[str]]): Event ids. - project_names (Optional[Iterable[str]]): Project on which - event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. - users (Optional[Iterable[str]]): Filtering by users - who created/triggered an event. - include_logs (Optional[bool]): Query also log events. - has_children (Optional[bool]): Event is with/without children - events. If 'None' then all events are returned, default. - newer_than (Optional[str]): Return only events newer than given - iso datetime string. - older_than (Optional[str]): Return only events older than given - iso datetime string. - fields (Optional[Iterable[str]]): Fields that should be received - for each event. - limit (Optional[int]): Limit number of events to be fetched. - order (Optional[SortOrder]): Order events in ascending - or descending order. It is recommended to set 'limit' - when used descending. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. - - Returns: - Generator[dict[str, Any]]: Available events matching filters. - - """ - if statuses is None and states is not None: - warnings.warn( - ( - "Used deprecated argument 'states' in 'get_events'." - " Use 'statuses' instead." - ), - DeprecationWarning - ) - statuses = states - - filters = {} - if not prepare_list_filters( - filters, - ("eventTopics", topics), - ("eventIds", event_ids), - ("projectNames", project_names), - ("eventStatuses", statuses), - ("eventUsers", users), - ): - return - - if include_logs is None: - include_logs = False - - for filter_key, filter_value in ( - ("includeLogsFilter", include_logs), - ("hasChildrenFilter", has_children), - ("newerThanFilter", newer_than), - ("olderThanFilter", older_than), - ): - if filter_value is not None: - filters[filter_key] = filter_value - - if not fields: - fields = self.get_default_fields_for_type("event") - - major, minor, patch, _, _ = self.server_version_tuple - use_states = (major, minor, patch) <= (1, 5, 6) - - query = events_graphql_query(set(fields), order, use_states) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - if limit: - events_field = query.get_field_by_path("events") - events_field.set_limit(limit) - - for parsed_data in query.continuous_query(self): - for event in parsed_data["events"]: - yield event - - def update_event( - self, - event_id: str, - sender: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - status: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - progress: Optional[int] = None, - retries: Optional[int] = None, - ): - """Update event data. - - Args: - event_id (str): Event id. - sender (Optional[str]): New sender of event. - project_name (Optional[str]): New project name. - username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", - "in_progress", "finished", "failed", "aborted", "restarted" - description (Optional[str]): New description. - summary (Optional[dict[str, Any]]): New summary. - payload (Optional[dict[str, Any]]): New payload. - progress (Optional[int]): New progress. Range [0-100]. - retries (Optional[int]): New retries. - - """ - kwargs = { - key: value - for key, value in ( - ("sender", sender), - ("project", project_name), - ("user", username), - ("status", status), - ("description", description), - ("summary", summary), - ("payload", payload), - ("progress", progress), - ("retries", retries), - ) - if value is not None - } - - response = self.patch( - f"events/{event_id}", - **kwargs - ) - response.raise_for_status() - - def dispatch_event( - self, - topic: str, - sender: Optional[str] = None, - event_hash: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - depends_on: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[Dict[str, Any]] = None, - payload: Optional[Dict[str, Any]] = None, - finished: bool = True, - store: bool = True, - dependencies: Optional[List[str]] = None, - ): - """Dispatch event to server. - - Args: - topic (str): Event topic used for filtering of listeners. - sender (Optional[str]): Sender of event. - event_hash (Optional[str]): Event hash. - project_name (Optional[str]): Project name. - depends_on (Optional[str]): Add dependency to another event. - username (Optional[str]): Username which triggered event. - description (Optional[str]): Description of event. - summary (Optional[dict[str, Any]]): Summary of event that can - be used for simple filtering on listeners. - payload (Optional[dict[str, Any]]): Full payload of event data with - all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible - future processing otherwise is event send only - to active listeners. - dependencies (Optional[list[str]]): Deprecated. - List of event id dependencies. - - Returns: - RestApiResponse: Response from server. - - """ - if summary is None: - summary = {} - if payload is None: - payload = {} - event_data = { - "topic": topic, - "sender": sender, - "hash": event_hash, - "project": project_name, - "user": username, - "description": description, - "summary": summary, - "payload": payload, - "finished": finished, - "store": store, - } - if depends_on: - event_data["dependsOn"] = depends_on - - if dependencies: - warnings.warn( - ( - "Used deprecated argument 'dependencies' in" - " 'dispatch_event'. Use 'depends_on' instead." - ), - DeprecationWarning - ) - - response = self.post("events", **event_data) - response.raise_for_status() - return response - - def delete_event(self, event_id: str): - """Delete event by id. - - Supported since AYON server 1.6.0. - - Args: - event_id (str): Event id. - - Returns: - RestApiResponse: Response from server. - - """ - response = self.delete(f"events/{event_id}") - response.raise_for_status() - return response - - def enroll_event_job( - self, - source_topic: "Union[str, List[str]]", - target_topic: str, - sender: str, - description: Optional[str] = None, - sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, - max_retries: Optional[int] = None, - ignore_older_than: Optional[str] = None, - ignore_sender_types: Optional[str] = None, - ): - """Enroll job based on events. - - Enroll will find first unprocessed event with 'source_topic' and will - create new event with 'target_topic' for it and return the new event - data. - - Use 'sequential' to control that only single target event is created - at same time. Creation of new target events is blocked while there is - at least one unfinished event with target topic, when set to 'True'. - This helps when order of events matter and more than one process using - the same target is running at the same time. - - Make sure the new event has updated status to '"finished"' status - when you're done with logic - - Target topic should not clash with other processes/services. - - Created target event have 'dependsOn' key where is id of source topic. - - Use-case: - - Service 1 is creating events with topic 'my.leech' - - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1-n machines - - all events must be processed in a sequence by their creation - time and only one event can be processed at a time - - in this case 'sequential' should be set to 'True' so only - one machine is actually processing events, but if one goes - down there are other that can take place - - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1-n machines - - order of events is not important - - 'sequential' should be 'False' - - Args: - source_topic (Union[str, List[str]]): Source topic to enroll with - wildcards '*', or explicit list of topics. - target_topic (str): Topic of dependent event. - sender (str): Identifier of sender (e.g. service name or username). - description (Optional[str]): Human readable text shown - in target event. - sequential (Optional[bool]): The source topic must be processed - in sequence. - events_filter (Optional[dict[str, Any]]): Filtering conditions - to filter the source event. For more technical specifications - look to server backed 'ayon_server.sqlfilter.Filter'. - TODO: Add example of filters. - max_retries (Optional[int]): How many times can be event retried. - Default value is based on server (3 at the time of this PR). - ignore_older_than (Optional[int]): Ignore events older than - given number in days. - ignore_sender_types (Optional[List[str]]): Ignore events triggered - by given sender types. - - Returns: - Union[None, dict[str, Any]]: None if there is no event matching - filters. Created event with 'target_topic'. - - """ - kwargs = { - "sourceTopic": source_topic, - "targetTopic": target_topic, - "sender": sender, - } - major, minor, patch, _, _ = self.server_version_tuple - if max_retries is not None: - kwargs["maxRetries"] = max_retries - if sequential is not None: - kwargs["sequential"] = sequential - if description is not None: - kwargs["description"] = description - if events_filter is not None: - kwargs["filter"] = events_filter - if ( - ignore_older_than is not None - and (major, minor, patch) > (1, 5, 1) - ): - kwargs["ignoreOlderThan"] = ignore_older_than - if ignore_sender_types is not None: - if (major, minor, patch) <= (1, 5, 4): - raise ValueError( - "Ignore sender types are not supported for" - f" your version of server {self.server_version}." - ) - kwargs["ignoreSenderTypes"] = list(ignore_sender_types) - - response = self.post("enroll", **kwargs) - if response.status_code == 204: - return None - - if response.status_code == 503: - # Server is busy - self.log.info("Server is busy. Can't enroll event now.") - return None - - if response.status_code >= 400: - self.log.error(response.text) - return None - - return response.data - - def get_activities( - self, - project_name: str, - 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["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - ) -> Generator[Dict[str, Any], None, None]: - """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[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. - - Returns: - Generator[dict[str, Any]]: Available activities matching filters. - - """ - if not project_name: - return - filters = { - "projectName": project_name, - } - if reference_types is None: - reference_types = {"origin"} - - 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), order) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - if limit: - activities_field = query.get_field_by_path("activities") - activities_field.set_limit(limit) - - 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( - 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. - - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - 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}, - reference_types=reference_types, - fields=fields, - ): - 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, - ) -> str: - """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: - str: 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["id"] - - def update_activity( - self, - project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[Dict[str, Any]] = 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. - append_file_ids (Optional[bool]): Append file ids to existing - list of file ids. - data (Optional[Dict[str, Any]]): Update data in activity. - - """ - 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: - 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.patch( - f"projects/{project_name}/activities/{activity_id}", - **update_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, @@ -6992,45 +6387,6 @@ def send_batch_operations( raise_on_fail, ) - def send_activities_batch_operations( - self, - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True - ) -> List[Dict[str, Any]]: - """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, From a34be6fb8365ed22043f35560805d3fa9615b47b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:05:28 +0200 Subject: [PATCH 133/506] moved activities api --- automated_api.py | 2 + ayon_api/__init__.py | 24 +-- ayon_api/_activities.py | 292 ++++++++++++++++++++++++++ ayon_api/_api.py | 440 ++++++++++++++++++++-------------------- ayon_api/_base.py | 14 +- ayon_api/server_api.py | 7 +- 6 files changed, 541 insertions(+), 238 deletions(-) create mode 100644 ayon_api/_activities.py diff --git a/automated_api.py b/automated_api.py index f630ed123..1fff22de0 100644 --- a/automated_api.py +++ b/automated_api.py @@ -336,6 +336,7 @@ def prepare_api_functions(api_globals): from ayon_api.server_api import ( # noqa: E402 ServerAPI, _ActionsAPI, + _ActivitiesAPI, _AddonsAPI, _EventsAPI, _FoldersAPI, @@ -347,6 +348,7 @@ def prepare_api_functions(api_globals): functions = [] _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) + _items.extend(_ActivitiesAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) _items.extend(_EventsAPI.__dict__.items()) _items.extend(_FoldersAPI.__dict__.items()) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 66379e097..9b4a956bb 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -67,11 +67,6 @@ patch, get, delete, - get_activities, - get_activity_by_id, - create_activity, - update_activity, - delete_activity, download_file_to_stream, download_file, upload_file_from_stream, @@ -190,13 +185,18 @@ create_thumbnail, update_thumbnail, send_batch_operations, - send_activities_batch_operations, get_actions, trigger_action, get_action_config, set_action_config, take_action, abort_action, + get_activities, + get_activity_by_id, + create_activity, + update_activity, + delete_activity, + send_activities_batch_operations, get_addon_endpoint, get_addons_info, get_addon_url, @@ -331,11 +331,6 @@ "patch", "get", "delete", - "get_activities", - "get_activity_by_id", - "create_activity", - "update_activity", - "delete_activity", "download_file_to_stream", "download_file", "upload_file_from_stream", @@ -454,13 +449,18 @@ "create_thumbnail", "update_thumbnail", "send_batch_operations", - "send_activities_batch_operations", "get_actions", "trigger_action", "get_action_config", "set_action_config", "take_action", "abort_action", + "get_activities", + "get_activity_by_id", + "create_activity", + "update_activity", + "delete_activity", + "send_activities_batch_operations", "get_addon_endpoint", "get_addons_info", "get_addon_url", diff --git a/ayon_api/_activities.py b/ayon_api/_activities.py new file mode 100644 index 000000000..f9d7796eb --- /dev/null +++ b/ayon_api/_activities.py @@ -0,0 +1,292 @@ +import json +import typing +from typing import Optional, Iterable, Generator, Any + +from ._base import _BaseServerAPI +from .utils import ( + SortOrder, + prepare_list_filters, +) +from .graphql_queries import activities_graphql_query + +if typing.TYPE_CHECKING: + from .typing import ( + ActivityType, + ActivityReferenceType, + ) + + +class _ActivitiesAPI(_BaseServerAPI): + def get_activities( + self, + project_name: str, + 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["ActivityReferenceType"]] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + ) -> Generator[dict[str, Any], None, None]: + """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[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending + or descending order. It is recommended to set 'limit' + when used descending. + + Returns: + Generator[dict[str, Any]]: Available activities matching filters. + + """ + if not project_name: + return + filters = { + "projectName": project_name, + } + if reference_types is None: + reference_types = {"origin"} + + 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), order) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + if limit: + activities_field = query.get_field_by_path("activities") + activities_field.set_limit(limit) + + 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( + 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. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + 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}, + reference_types=reference_types, + fields=fields, + ): + 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, + ) -> str: + """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: + str: 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["id"] + + def update_activity( + self, + project_name: str, + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[dict[str, Any]] = 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. + append_file_ids (Optional[bool]): Append file ids to existing + list of file ids. + data (Optional[dict[str, Any]]): Update data in activity. + + """ + update_data = {} + major, minor, patch, _, _ = self.get_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: + 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.patch( + f"projects/{project_name}/activities/{activity_id}", + **update_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 send_activities_batch_operations( + self, + project_name: str, + operations: list[dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True + ) -> list[dict[str, Any]]: + """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, + ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index d6a6f8ba7..70bcda06d 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -904,186 +904,6 @@ def delete( ) -def get_activities( - project_name: str, - 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["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, -) -> Generator[Dict[str, Any], None, None]: - """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[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. - - Returns: - Generator[dict[str, Any]]: Available activities matching filters. - - """ - con = get_server_api_connection() - return con.get_activities( - project_name=project_name, - activity_ids=activity_ids, - activity_types=activity_types, - entity_ids=entity_ids, - entity_names=entity_names, - entity_type=entity_type, - changed_after=changed_after, - changed_before=changed_before, - reference_types=reference_types, - fields=fields, - limit=limit, - order=order, - ) - - -def get_activity_by_id( - 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. - - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - 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( - project_name=project_name, - activity_id=activity_id, - reference_types=reference_types, - fields=fields, - ) - - -def create_activity( - 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, -) -> str: - """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: - str: Activity id. - - """ - con = get_server_api_connection() - return con.create_activity( - project_name=project_name, - entity_id=entity_id, - entity_type=entity_type, - activity_type=activity_type, - activity_id=activity_id, - body=body, - file_ids=file_ids, - timestamp=timestamp, - data=data, - ) - - -def update_activity( - project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[List[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[Dict[str, Any]] = 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. - 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() - return con.update_activity( - project_name=project_name, - activity_id=activity_id, - body=body, - file_ids=file_ids, - append_file_ids=append_file_ids, - data=data, - ) - - -def delete_activity( - 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. - - """ - con = get_server_api_connection() - return con.delete_activity( - project_name=project_name, - activity_id=activity_id, - ) - - def download_file_to_stream( endpoint: str, stream: "StreamType", @@ -4862,46 +4682,6 @@ def send_batch_operations( ) -def send_activities_batch_operations( - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """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( - project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, - ) - - def get_actions( project_name: Optional[str] = None, entity_type: Optional["ActionEntityTypes"] = None, @@ -5122,6 +4902,226 @@ def abort_action( ) +def get_activities( + project_name: str, + 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["ActivityReferenceType"]] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, +) -> Generator[dict[str, Any], None, None]: + """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[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending + or descending order. It is recommended to set 'limit' + when used descending. + + Returns: + Generator[dict[str, Any]]: Available activities matching filters. + + """ + con = get_server_api_connection() + return con.get_activities( + project_name=project_name, + activity_ids=activity_ids, + activity_types=activity_types, + entity_ids=entity_ids, + entity_names=entity_names, + entity_type=entity_type, + changed_after=changed_after, + changed_before=changed_before, + reference_types=reference_types, + fields=fields, + limit=limit, + order=order, + ) + + +def get_activity_by_id( + 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. + + Args: + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + 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( + project_name=project_name, + activity_id=activity_id, + reference_types=reference_types, + fields=fields, + ) + + +def create_activity( + 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, +) -> str: + """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: + str: Activity id. + + """ + con = get_server_api_connection() + return con.create_activity( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + activity_type=activity_type, + activity_id=activity_id, + body=body, + file_ids=file_ids, + timestamp=timestamp, + data=data, + ) + + +def update_activity( + project_name: str, + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[dict[str, Any]] = 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. + 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() + return con.update_activity( + project_name=project_name, + activity_id=activity_id, + body=body, + file_ids=file_ids, + append_file_ids=append_file_ids, + data=data, + ) + + +def delete_activity( + 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. + + """ + con = get_server_api_connection() + return con.delete_activity( + project_name=project_name, + activity_id=activity_id, + ) + + +def send_activities_batch_operations( + project_name: str, + operations: list, + can_fail: bool = False, + raise_on_fail: bool = True, +) -> list: + """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( + project_name=project_name, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, + ) + + def get_addon_endpoint( addon_name: str, addon_version: str, diff --git a/ayon_api/_base.py b/ayon_api/_base.py index faf8c81bf..a742289c2 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -1,7 +1,7 @@ from __future__ import annotations import typing -from typing import Optional +from typing import Optional, Any import requests @@ -12,6 +12,9 @@ class _BaseServerAPI: + def get_server_version(self) -> str: + raise NotImplementedError() + def get_server_version_tuple(self) -> "ServerVersion": raise NotImplementedError() @@ -94,3 +97,12 @@ def _prepare_fields( def _convert_entity_data(self, entity: "AnyEntityDict"): raise NotImplementedError() + + def _send_batch_operations( + self, + uri: str, + operations: list[dict[str, Any]], + can_fail: bool, + raise_on_fail: bool + ) -> list[dict[str, Any]]: + raise NotImplementedError() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 9dff80799..ca6413822 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -96,13 +96,13 @@ get_default_site_id, NOT_SET, get_media_mime_type, - SortOrder, get_machine_name, fill_own_attribs, prepare_list_filters, PatternType, ) from ._actions import _ActionsAPI +from ._activities import _ActivitiesAPI from ._addons import _AddonsAPI from ._events import _EventsAPI from ._folders import _FoldersAPI @@ -114,8 +114,6 @@ from typing import Union from .typing import ( ServerVersion, - ActivityType, - ActivityReferenceType, AttributeScope, AttributeSchemaDataDict, AttributeSchemaDict, @@ -128,13 +126,11 @@ SecretDict, AnyEntityDict, - FolderDict, TaskDict, ProductDict, VersionDict, RepresentationDict, WorkfileInfoDict, - FlatFolderDict, ProjectHierarchyDict, ProductTypeDict, @@ -369,6 +365,7 @@ def as_user(self, username): class ServerAPI( _ActionsAPI, + _ActivitiesAPI, _AddonsAPI, _EventsAPI, _FoldersAPI, From f876275ebe54b34298dde2a74a0df04ec24b3845 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:19:24 +0200 Subject: [PATCH 134/506] move few things to utils --- ayon_api/exceptions.py | 11 ++++ ayon_api/server_api.py | 129 +---------------------------------------- ayon_api/utils.py | 124 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 135 insertions(+), 129 deletions(-) diff --git a/ayon_api/exceptions.py b/ayon_api/exceptions.py index 32c786834..55343b1e8 100644 --- a/ayon_api/exceptions.py +++ b/ayon_api/exceptions.py @@ -1,5 +1,16 @@ import copy +try: + # This should be used if 'requests' have it available + from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError +except ImportError: + # Older versions of 'requests' don't have custom exception for json + # decode error + try: + from simplejson import JSONDecodeError as RequestsJSONDecodeError + except ImportError: + from json import JSONDecodeError as RequestsJSONDecodeError + class UrlError(Exception): """Url cannot be parsed as url. diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index ca6413822..52312d798 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -137,8 +137,6 @@ StreamType, ) -JSONDecodeError = getattr(json, "JSONDecodeError", ValueError) - _PLACEHOLDER = object() VERSION_REGEX = re.compile( @@ -150,116 +148,6 @@ ) -def _get_description(response): - if HTTPStatus is None: - return str(response.orig_response) - return HTTPStatus(response.status).description - - -class RestApiResponse(object): - """API Response.""" - - def __init__(self, response, data=None): - if response is None: - status_code = 500 - else: - status_code = response.status_code - self._response = response - self.status = status_code - self._data = data - - @property - def text(self): - if self._response is None: - return self.detail - return self._response.text - - @property - def orig_response(self): - return self._response - - @property - def headers(self): - if self._response is None: - return {} - return self._response.headers - - @property - def data(self): - if self._data is None: - try: - self._data = self.orig_response.json() - except RequestsJSONDecodeError: - self._data = {} - return self._data - - @property - def content(self): - if self._response is None: - return b"" - return self._response.content - - @property - def content_type(self) -> Optional[str]: - return self.headers.get("Content-Type") - - @property - def detail(self): - detail = self.get("detail") - if detail: - return detail - return _get_description(self) - - @property - def status_code(self) -> int: - return self.status - - @property - def ok(self) -> bool: - if self._response is not None: - return self._response.ok - return False - - def raise_for_status(self, message=None): - if self._response is None: - if self._data and self._data.get("detail"): - raise ServerError(self._data["detail"]) - raise ValueError("Response is not available.") - - if self.status_code == 401: - raise UnauthorizedError("Missing or invalid authentication token") - try: - self._response.raise_for_status() - except requests.exceptions.HTTPError as exc: - if message is None: - message = str(exc) - raise HTTPRequestError(message, exc.response) - - def __enter__(self, *args, **kwargs): - return self._response.__enter__(*args, **kwargs) - - def __contains__(self, key): - return key in self.data - - def __repr__(self): - return f"<{self.__class__.__name__} [{self.status}]>" - - def __len__(self): - return int(200 <= self.status < 400) - - def __bool__(self): - return 200 <= self.status < 400 - - def __getitem__(self, key): - return self.data[key] - - def get(self, key, default=None): - data = self.data - if isinstance(data, dict): - return self.data.get(key, default) - return default - - class GraphQlResponse: """GraphQl response.""" @@ -1400,22 +1288,7 @@ def _do_rest_request(self, function, url, **kwargs): if new_response is not None: return new_response - content_type = response.headers.get("Content-Type") - if content_type == "application/json": - try: - new_response = RestApiResponse(response) - except JSONDecodeError: - new_response = RestApiResponse( - None, - { - "detail": "The response is not a JSON: {}".format( - response.text) - } - ) - - else: - new_response = RestApiResponse(response) - + new_response = RestApiResponse(response) self.log.debug(f"Response {str(new_response)}") return new_response diff --git a/ayon_api/utils.py b/ayon_api/utils.py index dc5349ef7..7f04d64cd 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -23,7 +23,19 @@ DEFAULT_VARIANT_ENV_KEY, SITE_ID_ENV_KEY, ) -from .exceptions import UrlError +from .exceptions import ( + UrlError, + ServerError, + UnauthorizedError, + HTTPRequestError, + RequestsJSONDecodeError, +) + +try: + from http import HTTPStatus +except ImportError: + HTTPStatus = None + if typing.TYPE_CHECKING: from typing import Union @@ -84,6 +96,116 @@ class RequestTypes: delete = RequestType("DELETE") +def _get_description(response): + if HTTPStatus is None: + return str(response.orig_response) + return HTTPStatus(response.status).description + + +class RestApiResponse(object): + """API Response.""" + + def __init__(self, response, data=None): + if response is None: + status_code = 500 + else: + status_code = response.status_code + self._response = response + self.status = status_code + self._data = data + + @property + def text(self): + if self._response is None: + return self.detail + return self._response.text + + @property + def orig_response(self): + return self._response + + @property + def headers(self): + if self._response is None: + return {} + return self._response.headers + + @property + def data(self): + if self._data is None: + try: + self._data = self.orig_response.json() + except RequestsJSONDecodeError: + self._data = {} + return self._data + + @property + def content(self): + if self._response is None: + return b"" + return self._response.content + + @property + def content_type(self) -> Optional[str]: + return self.headers.get("Content-Type") + + @property + def detail(self): + detail = self.get("detail") + if detail: + return detail + return _get_description(self) + + @property + def status_code(self) -> int: + return self.status + + @property + def ok(self) -> bool: + if self._response is not None: + return self._response.ok + return False + + def raise_for_status(self, message=None): + if self._response is None: + if self._data and self._data.get("detail"): + raise ServerError(self._data["detail"]) + raise ValueError("Response is not available.") + + if self.status_code == 401: + raise UnauthorizedError("Missing or invalid authentication token") + try: + self._response.raise_for_status() + except requests.exceptions.HTTPError as exc: + if message is None: + message = str(exc) + raise HTTPRequestError(message, exc.response) + + def __enter__(self, *args, **kwargs): + return self._response.__enter__(*args, **kwargs) + + def __contains__(self, key): + return key in self.data + + def __repr__(self): + return f"<{self.__class__.__name__} [{self.status}]>" + + def __len__(self): + return int(200 <= self.status < 400) + + def __bool__(self): + return 200 <= self.status < 400 + + def __getitem__(self, key): + return self.data[key] + + def get(self, key, default=None): + data = self.data + if isinstance(data, dict): + return self.data.get(key, default) + return default + + def fill_own_attribs(entity: "AnyEntityDict") -> None: """Fill own attributes. From 462f9e2480a5d0d039adf0dfae07f8f174c4885b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:19:46 +0200 Subject: [PATCH 135/506] move thumbnails api to separate file --- automated_api.py | 2 + ayon_api/__init__.py | 32 +-- ayon_api/_api.py | 468 ++++++++++++++++++++-------------------- ayon_api/_thumbnails.py | 304 ++++++++++++++++++++++++++ ayon_api/server_api.py | 315 +-------------------------- 5 files changed, 559 insertions(+), 562 deletions(-) create mode 100644 ayon_api/_thumbnails.py diff --git a/automated_api.py b/automated_api.py index 1fff22de0..e47434efd 100644 --- a/automated_api.py +++ b/automated_api.py @@ -343,6 +343,7 @@ def prepare_api_functions(api_globals): _LinksAPI, _ListsAPI, _ProjectsAPI, + _ThumbnailsAPI, ) functions = [] @@ -355,6 +356,7 @@ def prepare_api_functions(api_globals): _items.extend(_LinksAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) _items.extend(_ProjectsAPI.__dict__.items()) + _items.extend(_ThumbnailsAPI.__dict__.items()) processed = set() for attr_name, attr in _items: diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 9b4a956bb..b6272a464 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -176,14 +176,6 @@ get_workfiles_info, get_workfile_info, get_workfile_info_by_id, - get_thumbnail_by_id, - get_thumbnail, - get_folder_thumbnail, - get_task_thumbnail, - get_version_thumbnail, - get_workfile_thumbnail, - create_thumbnail, - update_thumbnail, send_batch_operations, get_actions, trigger_action, @@ -261,6 +253,14 @@ create_project, update_project, delete_project, + get_thumbnail_by_id, + get_thumbnail, + get_folder_thumbnail, + get_task_thumbnail, + get_version_thumbnail, + get_workfile_thumbnail, + create_thumbnail, + update_thumbnail, ) @@ -440,14 +440,6 @@ "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", - "get_thumbnail_by_id", - "get_thumbnail", - "get_folder_thumbnail", - "get_task_thumbnail", - "get_version_thumbnail", - "get_workfile_thumbnail", - "create_thumbnail", - "update_thumbnail", "send_batch_operations", "get_actions", "trigger_action", @@ -525,4 +517,12 @@ "create_project", "update_project", "delete_project", + "get_thumbnail_by_id", + "get_thumbnail", + "get_folder_thumbnail", + "get_task_thumbnail", + "get_version_thumbnail", + "get_workfile_thumbnail", + "create_thumbnail", + "update_thumbnail", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 70bcda06d..678720202 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4408,240 +4408,6 @@ def get_workfile_info_by_id( ) -def get_thumbnail_by_id( - project_name: str, - thumbnail_id: str, -) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. - - Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - con = get_server_api_connection() - return con.get_thumbnail_by_id( - project_name=project_name, - thumbnail_id=thumbnail_id, - ) - - -def get_thumbnail( - project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Get thumbnail from server. - - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. - - Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - con = get_server_api_connection() - return con.get_thumbnail( - project_name=project_name, - entity_type=entity_type, - entity_id=entity_id, - thumbnail_id=thumbnail_id, - ) - - -def get_folder_thumbnail( - project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. - - Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - con = get_server_api_connection() - return con.get_folder_thumbnail( - project_name=project_name, - folder_id=folder_id, - thumbnail_id=thumbnail_id, - ) - - -def get_task_thumbnail( - project_name: str, - task_id: str, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - con = get_server_api_connection() - return con.get_task_thumbnail( - project_name=project_name, - task_id=task_id, - ) - - -def get_version_thumbnail( - project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. - - Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - con = get_server_api_connection() - return con.get_version_thumbnail( - project_name=project_name, - version_id=version_id, - thumbnail_id=thumbnail_id, - ) - - -def get_workfile_thumbnail( - project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. - - Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - con = get_server_api_connection() - return con.get_workfile_thumbnail( - project_name=project_name, - workfile_id=workfile_id, - thumbnail_id=thumbnail_id, - ) - - -def create_thumbnail( - project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, -) -> str: - """Create new thumbnail on server from passed path. - - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. - - Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. - - """ - con = get_server_api_connection() - return con.create_thumbnail( - project_name=project_name, - src_filepath=src_filepath, - thumbnail_id=thumbnail_id, - ) - - -def update_thumbnail( - project_name: str, - thumbnail_id: str, - src_filepath: str, -): - """Change thumbnail content by id. - - Update can be also used to create new thumbnail. - - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. - - Raises: - ValueError: When thumbnail source cannot be processed. - - """ - con = get_server_api_connection() - return con.update_thumbnail( - project_name=project_name, - thumbnail_id=thumbnail_id, - src_filepath=src_filepath, - ) - - def send_batch_operations( project_name: str, operations: List[Dict[str, Any]], @@ -7317,3 +7083,237 @@ def delete_project( return con.delete_project( project_name=project_name, ) + + +def get_thumbnail_by_id( + project_name: str, + thumbnail_id: str, +) -> ThumbnailContent: + """Get thumbnail from server by id. + + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_thumbnail_by_id( + project_name=project_name, + thumbnail_id=thumbnail_id, + ) + + +def get_thumbnail( + project_name: str, + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Get thumbnail from server. + + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_thumbnail( + project_name=project_name, + entity_type=entity_type, + entity_id=entity_id, + thumbnail_id=thumbnail_id, + ) + + +def get_folder_thumbnail( + project_name: str, + folder_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. + + Args: + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_folder_thumbnail( + project_name=project_name, + folder_id=folder_id, + thumbnail_id=thumbnail_id, + ) + + +def get_task_thumbnail( + project_name: str, + task_id: str, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_task_thumbnail( + project_name=project_name, + task_id=task_id, + ) + + +def get_version_thumbnail( + project_name: str, + version_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. + + Args: + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_version_thumbnail( + project_name=project_name, + version_id=version_id, + thumbnail_id=thumbnail_id, + ) + + +def get_workfile_thumbnail( + project_name: str, + workfile_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + con = get_server_api_connection() + return con.get_workfile_thumbnail( + project_name=project_name, + workfile_id=workfile_id, + thumbnail_id=thumbnail_id, + ) + + +def create_thumbnail( + project_name: str, + src_filepath: str, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + con = get_server_api_connection() + return con.create_thumbnail( + project_name=project_name, + src_filepath=src_filepath, + thumbnail_id=thumbnail_id, + ) + + +def update_thumbnail( + project_name: str, + thumbnail_id: str, + src_filepath: str, +): + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + con = get_server_api_connection() + return con.update_thumbnail( + project_name=project_name, + thumbnail_id=thumbnail_id, + src_filepath=src_filepath, + ) diff --git a/ayon_api/_thumbnails.py b/ayon_api/_thumbnails.py new file mode 100644 index 000000000..39fb86602 --- /dev/null +++ b/ayon_api/_thumbnails.py @@ -0,0 +1,304 @@ +import os +import warnings +from typing import Optional + +from ._base import _BaseServerAPI +from .utils import ( + get_media_mime_type, + ThumbnailContent, + RequestTypes, + RestApiResponse, +) + + +class _ThumbnailsAPI(_BaseServerAPI): + def get_thumbnail_by_id( + self, project_name: str, thumbnail_id: str + ) -> ThumbnailContent: + """Get thumbnail from server by id. + + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + response = self.raw_get( + f"projects/{project_name}/thumbnails/{thumbnail_id}" + ) + return self._prepare_thumbnail_content(project_name, response) + + def get_thumbnail( + self, + project_name: str, + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Get thumbnail from server. + + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. + + Args: + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) + + if entity_type in ( + "folder", + "task", + "version", + "workfile", + ): + entity_type += "s" + + response = self.raw_get( + f"projects/{project_name}/{entity_type}/{entity_id}/thumbnail" + ) + return self._prepare_thumbnail_content(project_name, response) + + def get_folder_thumbnail( + self, + project_name: str, + folder_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. + + Args: + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_folder_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) + return self.get_thumbnail( + project_name, "folder", folder_id + ) + + def get_task_thumbnail( + self, + project_name: str, + task_id: str, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + return self.get_thumbnail(project_name, "task", task_id) + + def get_version_thumbnail( + self, + project_name: str, + version_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. + + Args: + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_version_thumbnail' got 'thumbnail_id' which" + " is deprecated and will be removed in future version." + ), + DeprecationWarning + ) + return self.get_thumbnail( + project_name, "version", version_id + ) + + def get_workfile_thumbnail( + self, + project_name: str, + workfile_id: str, + thumbnail_id: Optional[str] = None, + ) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. + + """ + if thumbnail_id: + warnings.warn( + ( + "Function 'get_workfile_thumbnail' got 'thumbnail_id'" + " which is deprecated and will be removed in future" + " version." + ), + DeprecationWarning + ) + return self.get_thumbnail( + project_name, "workfile", workfile_id + ) + + def create_thumbnail( + self, + project_name: str, + src_filepath: str, + thumbnail_id: Optional[str] = None, + ) -> str: + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + if thumbnail_id: + self.update_thumbnail( + project_name, + thumbnail_id, + src_filepath + ) + return thumbnail_id + + mime_type = get_media_mime_type(src_filepath) + response = self.upload_file( + f"projects/{project_name}/thumbnails", + src_filepath, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + return response.json()["id"] + + def update_thumbnail( + self, project_name: str, thumbnail_id: str, src_filepath: str + ): + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + mime_type = get_media_mime_type(src_filepath) + response = self.upload_file( + f"projects/{project_name}/thumbnails/{thumbnail_id}", + src_filepath, + request_type=RequestTypes.put, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + + def _prepare_thumbnail_content( + self, + project_name: str, + response: RestApiResponse, + ) -> ThumbnailContent: + content = None + content_type = response.content_type + + # It is expected the response contains thumbnail id otherwise the + # content cannot be cached and filepath returned + thumbnail_id = response.headers.get("X-Thumbnail-Id") + if thumbnail_id is not None: + content = response.content + + return ThumbnailContent( + project_name, thumbnail_id, content, content_type + ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 52312d798..96cfe4891 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -16,29 +16,12 @@ import copy import uuid import warnings -import itertools from contextlib import contextmanager import typing from typing import Optional, Iterable, Tuple, Generator, Dict, List, Set, Any -try: - from http import HTTPStatus -except ImportError: - HTTPStatus = None - import requests -try: - # This should be used if 'requests' have it available - from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError -except ImportError: - # Older versions of 'requests' don't have custom exception for json - # decode error - try: - from simplejson import JSONDecodeError as RequestsJSONDecodeError - except ImportError: - from json import JSONDecodeError as RequestsJSONDecodeError - from .constants import ( SERVER_RETRIES_ENV_KEY, DEFAULT_FOLDER_TYPE_FIELDS, @@ -76,12 +59,11 @@ UnauthorizedError, AuthenticationError, ServerNotReached, - ServerError, - HTTPRequestError, ) from .utils import ( RequestType, RequestTypes, + RestApiResponse, RepresentationParents, RepresentationHierarchy, prepare_query_string, @@ -90,7 +72,6 @@ entity_data_json_default, failed_json_default, TransferProgress, - ThumbnailContent, get_default_timeout, get_default_settings_variant, get_default_site_id, @@ -109,6 +90,7 @@ from ._links import _LinksAPI from ._lists import _ListsAPI from ._projects import _ProjectsAPI +from ._thumbnails import _ThumbnailsAPI if typing.TYPE_CHECKING: from typing import Union @@ -132,7 +114,6 @@ RepresentationDict, WorkfileInfoDict, - ProjectHierarchyDict, ProductTypeDict, StreamType, ) @@ -260,6 +241,7 @@ class ServerAPI( _LinksAPI, _ListsAPI, _ProjectsAPI, + _ThumbnailsAPI, ): """Base handler of connection to server. @@ -5926,297 +5908,6 @@ def get_workfile_info_by_id( return workfile_info return None - def _prepare_thumbnail_content( - self, - project_name: str, - response: RestApiResponse, - ) -> ThumbnailContent: - content = None - content_type = response.content_type - - # It is expected the response contains thumbnail id otherwise the - # content cannot be cached and filepath returned - thumbnail_id = response.headers.get("X-Thumbnail-Id") - if thumbnail_id is not None: - content = response.content - - return ThumbnailContent( - project_name, thumbnail_id, content, content_type - ) - - def get_thumbnail_by_id( - self, project_name: str, thumbnail_id: str - ) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. - - Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - response = self.raw_get( - f"projects/{project_name}/thumbnails/{thumbnail_id}" - ) - return self._prepare_thumbnail_content(project_name, response) - - def get_thumbnail( - self, - project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Get thumbnail from server. - - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. - - Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." - ), - DeprecationWarning - ) - - if entity_type in ( - "folder", - "task", - "version", - "workfile", - ): - entity_type += "s" - - response = self.raw_get( - f"projects/{project_name}/{entity_type}/{entity_id}/thumbnail" - ) - return self._prepare_thumbnail_content(project_name, response) - - def get_folder_thumbnail( - self, - project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. - - Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_folder_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." - ), - DeprecationWarning - ) - return self.get_thumbnail( - project_name, "folder", folder_id - ) - - def get_task_thumbnail( - self, - project_name: str, - task_id: str, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - return self.get_thumbnail(project_name, "task", task_id) - - def get_version_thumbnail( - self, - project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. - - Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_version_thumbnail' got 'thumbnail_id' which" - " is deprecated and will be removed in future version." - ), - DeprecationWarning - ) - return self.get_thumbnail( - project_name, "version", version_id - ) - - def get_workfile_thumbnail( - self, - project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, - ) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. - - Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. - - Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. - - """ - if thumbnail_id: - warnings.warn( - ( - "Function 'get_workfile_thumbnail' got 'thumbnail_id'" - " which is deprecated and will be removed in future" - " version." - ), - DeprecationWarning - ) - return self.get_thumbnail( - project_name, "workfile", workfile_id - ) - - def create_thumbnail( - self, - project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, - ) -> str: - """Create new thumbnail on server from passed path. - - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. - - Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. - - """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") - - if thumbnail_id: - self.update_thumbnail( - project_name, - thumbnail_id, - src_filepath - ) - return thumbnail_id - - mime_type = get_media_mime_type(src_filepath) - response = self.upload_file( - f"projects/{project_name}/thumbnails", - src_filepath, - request_type=RequestTypes.post, - headers={"Content-Type": mime_type}, - ) - response.raise_for_status() - return response.json()["id"] - - def update_thumbnail( - self, project_name: str, thumbnail_id: str, src_filepath: str - ): - """Change thumbnail content by id. - - Update can be also used to create new thumbnail. - - Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. - - Raises: - ValueError: When thumbnail source cannot be processed. - - """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") - - mime_type = get_media_mime_type(src_filepath) - response = self.upload_file( - f"projects/{project_name}/thumbnails/{thumbnail_id}", - src_filepath, - request_type=RequestTypes.put, - headers={"Content-Type": mime_type}, - ) - response.raise_for_status() - # --- Batch operations processing --- def send_batch_operations( self, From f00f4e6effa4f7f33d22c944b272ca871c3cd4a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:25:49 +0200 Subject: [PATCH 136/506] moved workfiles info into separate file --- automated_api.py | 2 + ayon_api/__init__.py | 12 +-- ayon_api/_api.py | 230 ++++++++++++++++++++--------------------- ayon_api/_base.py | 2 + ayon_api/_workfiles.py | 183 ++++++++++++++++++++++++++++++++ ayon_api/server_api.py | 177 +------------------------------ 6 files changed, 311 insertions(+), 295 deletions(-) create mode 100644 ayon_api/_workfiles.py diff --git a/automated_api.py b/automated_api.py index e47434efd..bdadfd6e6 100644 --- a/automated_api.py +++ b/automated_api.py @@ -344,6 +344,7 @@ def prepare_api_functions(api_globals): _ListsAPI, _ProjectsAPI, _ThumbnailsAPI, + _WorkfilesAPI, ) functions = [] @@ -357,6 +358,7 @@ def prepare_api_functions(api_globals): _items.extend(_ListsAPI.__dict__.items()) _items.extend(_ProjectsAPI.__dict__.items()) _items.extend(_ThumbnailsAPI.__dict__.items()) + _items.extend(_WorkfilesAPI.__dict__.items()) processed = set() for attr_name, attr in _items: diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index b6272a464..73f45e13f 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -173,9 +173,6 @@ create_representation, update_representation, delete_representation, - get_workfiles_info, - get_workfile_info, - get_workfile_info_by_id, send_batch_operations, get_actions, trigger_action, @@ -261,6 +258,9 @@ get_workfile_thumbnail, create_thumbnail, update_thumbnail, + get_workfiles_info, + get_workfile_info, + get_workfile_info_by_id, ) @@ -437,9 +437,6 @@ "create_representation", "update_representation", "delete_representation", - "get_workfiles_info", - "get_workfile_info", - "get_workfile_info_by_id", "send_batch_operations", "get_actions", "trigger_action", @@ -525,4 +522,7 @@ "get_workfile_thumbnail", "create_thumbnail", "update_thumbnail", + "get_workfiles_info", + "get_workfile_info", + "get_workfile_info_by_id", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 678720202..4abf5beaa 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4293,121 +4293,6 @@ def delete_representation( ) -def get_workfiles_info( - project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - paths: Optional[Iterable[str]] = None, - path_regex: Optional[str] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["WorkfileInfoDict", None, None]: - """Workfile info entities by passed filters. - - Args: - project_name (str): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used - for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used - for filtering. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. - - """ - con = get_server_api_connection() - return con.get_workfiles_info( - project_name=project_name, - workfile_ids=workfile_ids, - task_ids=task_ids, - paths=paths, - path_regex=path_regex, - statuses=statuses, - tags=tags, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_workfile_info( - project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - con = get_server_api_connection() - return con.get_workfile_info( - project_name=project_name, - task_id=task_id, - path=path, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_workfile_info_by_id( - project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. - - Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - con = get_server_api_connection() - return con.get_workfile_info_by_id( - project_name=project_name, - workfile_id=workfile_id, - fields=fields, - own_attributes=own_attributes, - ) - - def send_batch_operations( project_name: str, operations: List[Dict[str, Any]], @@ -7317,3 +7202,118 @@ def update_thumbnail( thumbnail_id=thumbnail_id, src_filepath=src_filepath, ) + + +def get_workfiles_info( + project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + paths: Optional[Iterable[str]] = None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> "Generator[WorkfileInfoDict, None, None]": + """Workfile info entities by passed filters. + + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. + + """ + con = get_server_api_connection() + return con.get_workfiles_info( + project_name=project_name, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, + statuses=statuses, + tags=tags, + has_links=has_links, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_workfile_info( + project_name: str, + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by task id and workfile path. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + con = get_server_api_connection() + return con.get_workfile_info( + project_name=project_name, + task_id=task_id, + path=path, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_workfile_info_by_id( + project_name: str, + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by id. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + con = get_server_api_connection() + return con.get_workfile_info_by_id( + project_name=project_name, + workfile_id=workfile_id, + fields=fields, + own_attributes=own_attributes, + ) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index a742289c2..d98869df0 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -10,6 +10,8 @@ if typing.TYPE_CHECKING: from .typing import AnyEntityDict, ServerVersion +_PLACEHOLDER = object() + class _BaseServerAPI: def get_server_version(self) -> str: diff --git a/ayon_api/_workfiles.py b/ayon_api/_workfiles.py new file mode 100644 index 000000000..2a624f607 --- /dev/null +++ b/ayon_api/_workfiles.py @@ -0,0 +1,183 @@ +import warnings +import typing +from typing import Optional, Iterable, Generator + +from ._base import _BaseServerAPI, _PLACEHOLDER +from .graphql_queries import workfiles_info_graphql_query + +if typing.TYPE_CHECKING: + from .typing import WorkfileInfoDict + + +class _WorkfilesAPI(_BaseServerAPI): + def get_workfiles_info( + self, + project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] =None, + paths: Optional[Iterable[str]] =None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str]=None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator["WorkfileInfoDict", None, None]: + """Workfile info entities by passed filters. + + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. + + """ + filters = {"projectName": project_name} + if task_ids is not None: + task_ids = set(task_ids) + if not task_ids: + return + filters["taskIds"] = list(task_ids) + + if paths is not None: + paths = set(paths) + if not paths: + return + filters["paths"] = list(paths) + + if path_regex is not None: + filters["workfilePathRegex"] = path_regex + + if workfile_ids is not None: + workfile_ids = set(workfile_ids) + if not workfile_ids: + return + filters["workfileIds"] = list(workfile_ids) + + if statuses is not None: + statuses = set(statuses) + if not statuses: + return + filters["workfileStatuses"] = list(statuses) + + if tags is not None: + tags = set(tags) + if not tags: + return + filters["workfileTags"] = list(tags) + + if has_links is not None: + filters["workfilehasLinks"] = has_links.upper() + + if not fields: + fields = self.get_default_fields_for_type("workfile") + else: + fields = set(fields) + self._prepare_fields("workfile", fields) + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for workfiles. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + query = workfiles_info_graphql_query(fields) + + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for workfile_info in parsed_data["project"]["workfiles"]: + self._convert_entity_data(workfile_info) + yield workfile_info + + def get_workfile_info( + self, + project_name: str, + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by task id and workfile path. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + if not task_id or not path: + return None + + for workfile_info in self.get_workfiles_info( + project_name, + task_ids=[task_id], + paths=[path], + fields=fields, + own_attributes=own_attributes + ): + return workfile_info + return None + + def get_workfile_info_by_id( + self, + project_name: str, + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by id. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + if not workfile_id: + return None + + for workfile_info in self.get_workfiles_info( + project_name, + workfile_ids=[workfile_id], + fields=fields, + own_attributes=own_attributes + ): + return workfile_info + return None diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 96cfe4891..b2d58d47f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -82,6 +82,7 @@ prepare_list_filters, PatternType, ) +from ._base import _PLACEHOLDER from ._actions import _ActionsAPI from ._activities import _ActivitiesAPI from ._addons import _AddonsAPI @@ -91,6 +92,7 @@ from ._lists import _ListsAPI from ._projects import _ProjectsAPI from ._thumbnails import _ThumbnailsAPI +from ._workfiles import _WorkfilesAPI if typing.TYPE_CHECKING: from typing import Union @@ -118,8 +120,6 @@ StreamType, ) -_PLACEHOLDER = object() - VERSION_REGEX = re.compile( r"(?P0|[1-9]\d*)" r"\.(?P0|[1-9]\d*)" @@ -242,6 +242,7 @@ class ServerAPI( _ListsAPI, _ProjectsAPI, _ThumbnailsAPI, + _WorkfilesAPI, ): """Base handler of connection to server. @@ -5736,178 +5737,6 @@ def delete_representation( ) response.raise_for_status() - def get_workfiles_info( - self, - project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] =None, - paths: Optional[Iterable[str]] =None, - path_regex: Optional[str] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - has_links: Optional[str]=None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Generator["WorkfileInfoDict", None, None]: - """Workfile info entities by passed filters. - - Args: - project_name (str): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used - for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used - for filtering. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. - - """ - filters = {"projectName": project_name} - if task_ids is not None: - task_ids = set(task_ids) - if not task_ids: - return - filters["taskIds"] = list(task_ids) - - if paths is not None: - paths = set(paths) - if not paths: - return - filters["paths"] = list(paths) - - if path_regex is not None: - filters["workfilePathRegex"] = path_regex - - if workfile_ids is not None: - workfile_ids = set(workfile_ids) - if not workfile_ids: - return - filters["workfileIds"] = list(workfile_ids) - - if statuses is not None: - statuses = set(statuses) - if not statuses: - return - filters["workfileStatuses"] = list(statuses) - - if tags is not None: - tags = set(tags) - if not tags: - return - filters["workfileTags"] = list(tags) - - if has_links is not None: - filters["workfilehasLinks"] = has_links.upper() - - if not fields: - fields = self.get_default_fields_for_type("workfile") - else: - fields = set(fields) - self._prepare_fields("workfile", fields) - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for workfiles. The" - " argument will be removed form function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - query = workfiles_info_graphql_query(fields) - - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for workfile_info in parsed_data["project"]["workfiles"]: - self._convert_entity_data(workfile_info) - yield workfile_info - - def get_workfile_info( - self, - project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. - - Args: - project_name (str): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - if not task_id or not path: - return None - - for workfile_info in self.get_workfiles_info( - project_name, - task_ids=[task_id], - paths=[path], - fields=fields, - own_attributes=own_attributes - ): - return workfile_info - return None - - def get_workfile_info_by_id( - self, - project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. - - Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. - - """ - if not workfile_id: - return None - - for workfile_info in self.get_workfiles_info( - project_name, - workfile_ids=[workfile_id], - fields=fields, - own_attributes=own_attributes - ): - return workfile_info - return None - # --- Batch operations processing --- def send_batch_operations( self, From 14b56b766075d12f254b2881f182526287f7e026 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:36:06 +0200 Subject: [PATCH 137/506] moved representations api to separate file --- automated_api.py | 2 + ayon_api/__init__.py | 48 +- ayon_api/_api.py | 2744 +++++++++++++++++----------------- ayon_api/_base.py | 16 +- ayon_api/_representations.py | 746 +++++++++ ayon_api/server_api.py | 727 +-------- 6 files changed, 2163 insertions(+), 2120 deletions(-) create mode 100644 ayon_api/_representations.py diff --git a/automated_api.py b/automated_api.py index bdadfd6e6..a581dde58 100644 --- a/automated_api.py +++ b/automated_api.py @@ -345,6 +345,7 @@ def prepare_api_functions(api_globals): _ProjectsAPI, _ThumbnailsAPI, _WorkfilesAPI, + _RepresentationsAPI, ) functions = [] @@ -359,6 +360,7 @@ def prepare_api_functions(api_globals): _items.extend(_ProjectsAPI.__dict__.items()) _items.extend(_ThumbnailsAPI.__dict__.items()) _items.extend(_WorkfilesAPI.__dict__.items()) + _items.extend(_RepresentationsAPI.__dict__.items()) processed = set() for attr_name, attr in _items: diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 73f45e13f..dd7b1a053 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -130,7 +130,6 @@ get_rest_task, get_rest_product, get_rest_version, - get_rest_representation, get_tasks, get_task_by_name, get_task_by_id, @@ -162,17 +161,6 @@ create_version, update_version, delete_version, - get_representations, - get_representation_by_id, - get_representation_by_name, - get_representations_hierarchy, - get_representation_hierarchy, - get_representations_parents, - get_representation_parents, - get_repre_ids_by_context_filters, - create_representation, - update_representation, - delete_representation, send_batch_operations, get_actions, trigger_action, @@ -261,6 +249,18 @@ get_workfiles_info, get_workfile_info, get_workfile_info_by_id, + get_rest_representation, + get_representations, + get_representation_by_id, + get_representation_by_name, + get_representations_hierarchy, + get_representation_hierarchy, + get_representations_parents, + get_representation_parents, + get_repre_ids_by_context_filters, + create_representation, + update_representation, + delete_representation, ) @@ -394,7 +394,6 @@ "get_rest_task", "get_rest_product", "get_rest_version", - "get_rest_representation", "get_tasks", "get_task_by_name", "get_task_by_id", @@ -426,17 +425,6 @@ "create_version", "update_version", "delete_version", - "get_representations", - "get_representation_by_id", - "get_representation_by_name", - "get_representations_hierarchy", - "get_representation_hierarchy", - "get_representations_parents", - "get_representation_parents", - "get_repre_ids_by_context_filters", - "create_representation", - "update_representation", - "delete_representation", "send_batch_operations", "get_actions", "trigger_action", @@ -525,4 +513,16 @@ "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", + "get_rest_representation", + "get_representations", + "get_representation_by_id", + "get_representation_by_name", + "get_representations_hierarchy", + "get_representation_hierarchy", + "get_representations_parents", + "get_representation_parents", + "get_repre_ids_by_context_filters", + "create_representation", + "update_representation", + "delete_representation", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 4abf5beaa..6df3bebb3 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -2613,17 +2613,6 @@ def get_rest_version( ) -def get_rest_representation( - project_name: str, - representation_id: str, -) -> Optional["RepresentationDict"]: - con = get_server_api_connection() - return con.get_rest_representation( - project_name=project_name, - representation_id=representation_id, - ) - - def get_tasks( project_name: str, task_ids: Optional[Iterable[str]] = None, @@ -3830,625 +3819,162 @@ def delete_version( ) -def get_representations( +def send_batch_operations( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - names_by_version_ids: Optional[Dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["RepresentationDict", None, None]: - """Get representation entities based on passed filters from server. - - .. todo:: + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """Post multiple CRUD operations to server. - Add separated function for 'names_by_version_ids' filtering. - Because can't be combined with others. + 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): Name of project where to look for versions. - representation_ids (Optional[Iterable[str]]): Representation ids - used for representation filtering. - representation_names (Optional[Iterable[str]]): Representation - names used for representation filtering. - version_ids (Optional[Iterable[str]]): Version ids used for - representation filtering. Versions are parents of - representations. - names_by_version_ids (Optional[Dict[str, Iterable[str]]]): Find - representations by names and version ids. This filter - discards all other filters. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + 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: - Generator[RepresentationDict, None, None]: Queried - representation entities. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_representations( + return con.send_batch_operations( project_name=project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - names_by_version_ids=names_by_version_ids, - statuses=statuses, - tags=tags, - active=active, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_representation_by_id( - project_name: str, - representation_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity from server based on id filter. +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> List["ActionManifestDict"]: + """Get actions for a context. Args: - project_name (str): Project where to look for representation. - representation_id (str): Id of representation. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. Returns: - Optional[RepresentationDict]: Queried representation - entity or None. + List[ActionManifestDict]: List of action manifests. """ con = get_server_api_connection() - return con.get_representation_by_id( + return con.get_actions( project_name=project_name, - representation_id=representation_id, - fields=fields, - own_attributes=own_attributes, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, ) -def get_representation_by_name( - project_name: str, - representation_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity by name and version id. +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. Args: - project_name (str): Project where to look for representation. - representation_name (str): Representation name. - version_id (str): Version id. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[RepresentationDict]: Queried representation entity - or None. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. """ con = get_server_api_connection() - return con.get_representation_by_name( + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - representation_name=representation_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_representations_hierarchy( - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Dict[str, RepresentationHierarchy]: - """Find representation with parents by representation id. - - Representation entity with parent entities up to project. - - Default fields are used when any fields are set to `None`. But it is - possible to pass in empty iterable (list, set, tuple) to skip - entity. +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. Returns: - dict[str, RepresentationHierarchy]: Parent entities by - representation id. - - """ - con = get_server_api_connection() - return con.get_representations_hierarchy( - project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, - ) - - -def get_representation_hierarchy( - project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Optional[RepresentationHierarchy]: - """Find representation parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. - - Returns: - RepresentationHierarchy: Representation hierarchy entities. - - """ - con = get_server_api_connection() - return con.get_representation_hierarchy( - project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, - ) - - -def get_representations_parents( - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Dict[str, RepresentationParents]: - """Find representations parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - - Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. - - """ - con = get_server_api_connection() - return con.get_representations_parents( - project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, - ) - - -def get_representation_parents( - project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Optional["RepresentationParents"]: - """Find representation parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - - Returns: - RepresentationParents: Representation parent entities. - - """ - con = get_server_api_connection() - return con.get_representation_parents( - project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, - ) - - -def get_repre_ids_by_context_filters( - project_name: str, - context_filters: Optional[Dict[str, Iterable[str]]], - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, -) -> List[str]: - """Find representation ids which match passed context filters. - - Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or - product name, product type and many more. This implementation gives - option to quickly filter representation based on representation data - in database. - - Context filters have defined structure. To define filter of nested - subfield use dot '.' as delimiter (For example 'task.name'). - Filter values can be regex filters. String or ``re.Pattern`` can - be used. - - Args: - project_name (str): Project where to look for representations. - context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Optional[Iterable[str]]): Representation - names, can be used as additional filter for representations - by their names. - version_ids (Optional[Iterable[str]]): Version ids, can be used - as additional filter for representations by their parent ids. - - Returns: - list[str]: Representation ids that match passed filters. - - Example: - The function returns just representation ids so if entities are - required for funtionality they must be queried afterwards by - their ids. - >>> project_name = "testProject" - >>> filters = { - ... "task.name": ["[aA]nimation"], - ... "product": [".*[Mm]ain"] - ... } - >>> repre_ids = get_repre_ids_by_context_filters( - ... project_name, filters) - >>> repres = get_representations(project_name, repre_ids) - - """ - con = get_server_api_connection() - return con.get_repre_ids_by_context_filters( - project_name=project_name, - context_filters=context_filters, - representation_names=representation_names, - version_ids=version_ids, - ) - - -def create_representation( - project_name: str, - name: str, - version_id: str, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - representation_id: Optional[str] = None, -) -> str: - """Create new representation. - - Args: - project_name (str): Project name. - name (str): Representation name. - version_id (str): Parent version id. - files (Optional[list[dict]]): Representation files information. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - representation_id (Optional[str]): Representation id. If not - passed new id is generated. - - Returns: - str: Representation id. - - """ - con = get_server_api_connection() - return con.create_representation( - project_name=project_name, - name=name, - version_id=version_id, - files=files, - attrib=attrib, - data=data, - traits=traits, - tags=tags, - status=status, - active=active, - representation_id=representation_id, - ) - - -def update_representation( - project_name: str, - representation_id: str, - name: Optional[str] = None, - version_id: Optional[str] = None, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, -): - """Update representation entity on server. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - representation_id (str): Representation id. - name (Optional[str]): New name. - version_id (Optional[str]): New version id. - files (Optional[list[dict]]): New files - information. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - traits (Optional[dict[str, Any]]): New traits. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - - """ - con = get_server_api_connection() - return con.update_representation( - project_name=project_name, - representation_id=representation_id, - name=name, - version_id=version_id, - files=files, - attrib=attrib, - data=data, - traits=traits, - tags=tags, - status=status, - active=active, - ) - - -def delete_representation( - project_name: str, - representation_id: str, -): - """Delete representation. - - Args: - project_name (str): Project name. - representation_id (str): Representation id to delete. - - """ - con = get_server_api_connection() - return con.delete_representation( - project_name=project_name, - representation_id=representation_id, - ) - - -def send_batch_operations( - project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD 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_batch_operations( - project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, - ) - - -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. - - Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. - - Returns: - List[ActionManifestDict]: List of action manifests. - - """ - con = get_server_api_connection() - return con.get_actions( - project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - mode=mode, - ) - - -def trigger_action( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. - - Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - - """ - con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - ) - - -def get_action_config( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. - - Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - - Returns: - ActionConfigResponse: Action configuration data. + ActionConfigResponse: Action configuration data. """ con = get_server_api_connection() @@ -6078,1242 +5604,1718 @@ def get_entities_links( ) -def get_folders_links( +def get_folders_links( + project_name: str, + folder_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query folders links from server. + + Args: + project_name (str): Project where links are. + folder_ids (Optional[Iterable[str]]): Ids of folders for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by folder ids. + + """ + con = get_server_api_connection() + return con.get_folders_links( + project_name=project_name, + folder_ids=folder_ids, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_folder_links( + project_name: str, + folder_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query folder links from server. + + Args: + project_name (str): Project where links are. + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of folder. + + """ + con = get_server_api_connection() + return con.get_folder_links( + project_name=project_name, + folder_id=folder_id, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_tasks_links( + project_name: str, + task_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query tasks links from server. + + Args: + project_name (str): Project where links are. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by task ids. + + """ + con = get_server_api_connection() + return con.get_tasks_links( + project_name=project_name, + task_ids=task_ids, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_task_links( + project_name: str, + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query task links from server. + + Args: + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of task. + + """ + con = get_server_api_connection() + return con.get_task_links( + project_name=project_name, + task_id=task_id, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_products_links( + project_name: str, + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. + + Args: + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by product ids. + + """ + con = get_server_api_connection() + return con.get_products_links( + project_name=project_name, + product_ids=product_ids, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_product_links( + project_name: str, + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query product links from server. + + Args: + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of product. + + """ + con = get_server_api_connection() + return con.get_product_links( + project_name=project_name, + product_id=product_id, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_versions_links( + project_name: str, + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. + + Args: + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by version ids. + + """ + con = get_server_api_connection() + return con.get_versions_links( + project_name=project_name, + version_ids=version_ids, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_version_links( + project_name: str, + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query version links from server. + + Args: + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of version. + + """ + con = get_server_api_connection() + return con.get_version_links( + project_name=project_name, + version_id=version_id, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_representations_links( project_name: str, - folder_ids: Optional[Iterable[str]] = None, + representation_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, link_direction: Optional["LinkDirection"] = None, ) -> dict[str, list[dict[str, Any]]]: - """Query folders links from server. + """Query representations links from server. Args: project_name (str): Project where links are. - folder_ids (Optional[Iterable[str]]): Ids of folders for which - links should be received. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. link_types (Optional[Iterable[str]]): Link type filters. link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: - dict[str, list[dict[str, Any]]]: Link info by folder ids. + dict[str, list[dict[str, Any]]]: Link info by representation ids. """ con = get_server_api_connection() - return con.get_folders_links( + return con.get_representations_links( project_name=project_name, - folder_ids=folder_ids, + representation_ids=representation_ids, link_types=link_types, link_direction=link_direction, ) -def get_folder_links( +def get_representation_links( project_name: str, - folder_id: str, + representation_id: str, link_types: Optional[Iterable[str]] = None, link_direction: Optional["LinkDirection"] = None, ) -> list[dict[str, Any]]: - """Query folder links from server. + """Query representation links from server. Args: project_name (str): Project where links are. - folder_id (str): Folder id for which links should be received. + representation_id (str): Representation id for which links + should be received. link_types (Optional[Iterable[str]]): Link type filters. link_direction (Optional[Literal["in", "out"]]): Link direction filter. Returns: - list[dict[str, Any]]: Link info of folder. + list[dict[str, Any]]: Link info of representation. """ con = get_server_api_connection() - return con.get_folder_links( + return con.get_representation_links( project_name=project_name, - folder_id=folder_id, + representation_id=representation_id, link_types=link_types, link_direction=link_direction, ) -def get_tasks_links( +def get_entity_lists( project_name: str, - task_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query tasks links from server. + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, +) -> Generator[Dict[str, Any], None, None]: + """Fetch entity lists from server. Args: - project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. + Generator[Dict[str, Any], None, None]: Entity list entities + matching defined filters. """ con = get_server_api_connection() - return con.get_tasks_links( + return con.get_entity_lists( project_name=project_name, - task_ids=task_ids, - link_types=link_types, - link_direction=link_direction, + list_ids=list_ids, + active=active, + fields=fields, ) -def get_task_links( +def get_entity_list_rest( project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query task links from server. + list_id: str, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using REST API. Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name. + list_id (str): Entity list id. Returns: - list[dict[str, Any]]: Link info of task. + Optional[Dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.get_task_links( + return con.get_entity_list_rest( project_name=project_name, - task_id=task_id, - link_types=link_types, - link_direction=link_direction, + list_id=list_id, ) -def get_products_links( +def get_entity_list_by_id( project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query products links from server. + list_id: str, + fields: Optional[Iterable[str]] = None, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using GraphQl. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. + + """ + con = get_server_api_connection() + return con.get_entity_list_by_id( + project_name=project_name, + list_id=list_id, + fields=fields, + ) + + +def create_entity_list( + project_name: str, + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + template: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[List[Dict[str, Any]]] = None, + list_id: Optional[str] = None, +) -> str: + """Create entity list. + + Args: + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. + + """ + con = get_server_api_connection() + return con.create_entity_list( + project_name=project_name, + entity_type=entity_type, + label=label, + list_type=list_type, + access=access, + attrib=attrib, + data=data, + tags=tags, + template=template, + owner=owner, + active=active, + items=items, + list_id=list_id, + ) + + +def update_entity_list( + project_name: str, + list_id: str, + *, + label: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, +) -> None: + """Update entity list. + + Args: + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + + """ + con = get_server_api_connection() + return con.update_entity_list( + project_name=project_name, + list_id=list_id, + label=label, + access=access, + attrib=attrib, + data=data, + tags=tags, + owner=owner, + active=active, + ) + + +def delete_entity_list( + project_name: str, + list_id: str, +) -> None: + """Delete entity list from project. + + Args: + project_name (str): Project name. + list_id (str): Entity list id that will be removed. + + """ + con = get_server_api_connection() + return con.delete_entity_list( + project_name=project_name, + list_id=list_id, + ) + + +def get_entity_list_attribute_definitions( + project_name: str, + list_id: str, +) -> List["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + + Returns: + List[EntityListAttributeDefinitionDict]: List of attribute + definitions. + + """ + con = get_server_api_connection() + return con.get_entity_list_attribute_definitions( + project_name=project_name, + list_id=list_id, + ) + - Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. +def set_entity_list_attribute_definitions( + project_name: str, + list_id: str, + attribute_definitions: List["EntityListAttributeDefinitionDict"], +) -> None: + """Set attribute definitioins on entity list. - Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. + Args: + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (List[EntityListAttributeDefinitionDict]): + List of attribute definitions. """ con = get_server_api_connection() - return con.get_products_links( + return con.set_entity_list_attribute_definitions( project_name=project_name, - product_ids=product_ids, - link_types=link_types, - link_direction=link_direction, + list_id=list_id, + attribute_definitions=attribute_definitions, ) -def get_product_links( +def create_entity_list_item( project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query product links from server. + list_id: str, + *, + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + item_id: Optional[str] = None, +) -> str: + """Create entity list item. Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. Returns: - list[dict[str, Any]]: Link info of product. + str: Item id. """ con = get_server_api_connection() - return con.get_product_links( + return con.create_entity_list_item( project_name=project_name, - product_id=product_id, - link_types=link_types, - link_direction=link_direction, + list_id=list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + item_id=item_id, ) -def get_versions_links( +def update_entity_list_items( project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query versions links from server. + list_id: str, + items: List[Dict[str, Any]], + mode: "EntityListItemMode", +) -> None: + """Update items in entity list. Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (List[Dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. """ con = get_server_api_connection() - return con.get_versions_links( + return con.update_entity_list_items( project_name=project_name, - version_ids=version_ids, - link_types=link_types, - link_direction=link_direction, + list_id=list_id, + items=items, + mode=mode, ) -def get_version_links( +def update_entity_list_item( project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query version links from server. + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, +) -> None: + """Update item in entity list. Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of version. + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. """ con = get_server_api_connection() - return con.get_version_links( + return con.update_entity_list_item( project_name=project_name, - version_id=version_id, - link_types=link_types, - link_direction=link_direction, + list_id=list_id, + item_id=item_id, + new_list_id=new_list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, ) -def get_representations_links( +def delete_entity_list_item( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query representations links from server. + list_id: str, + item_id: str, +) -> None: + """Delete item from entity list. Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. """ con = get_server_api_connection() - return con.get_representations_links( + return con.delete_entity_list_item( project_name=project_name, - representation_ids=representation_ids, - link_types=link_types, - link_direction=link_direction, + list_id=list_id, + item_id=item_id, ) -def get_representation_links( +def get_rest_project( project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query representation links from server. +) -> Optional["ProjectDict"]: + """Query project by name. + + This call returns project with anatomy data. Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project. Returns: - list[dict[str, Any]]: Link info of representation. + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. """ con = get_server_api_connection() - return con.get_representation_links( + return con.get_rest_project( project_name=project_name, - representation_id=representation_id, - link_types=link_types, - link_direction=link_direction, ) -def get_entity_lists( - project_name: str, - *, - list_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: - """Fetch entity lists from server. +def get_rest_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> Generator["ProjectDict", None, None]: + """Query available project entities. + + User must be logged in. Args: - project_name (str): Project name where entity lists are. - list_ids (Optional[Iterable[str]]): List of entity list ids to - fetch. - active (Optional[bool]): Filter by active state of entity lists. - fields (Optional[Iterable[str]]): Fields to fetch from server. + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. Returns: - Generator[Dict[str, Any], None, None]: Entity list entities - matching defined filters. + Generator[ProjectDict, None, None]: Available projects. """ con = get_server_api_connection() - return con.get_entity_lists( - project_name=project_name, - list_ids=list_ids, + return con.get_rest_projects( active=active, - fields=fields, + library=library, ) -def get_entity_list_rest( - project_name: str, - list_id: str, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using REST API. +def get_project_names( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> list[str]: + """Receive available project names. + + User must be logged in. Args: - project_name (str): Project name. - list_id (str): Entity list id. + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + list[str]: List of available project names. """ con = get_server_api_connection() - return con.get_entity_list_rest( - project_name=project_name, - list_id=list_id, + return con.get_project_names( + active=active, + library=library, ) -def get_entity_list_by_id( - project_name: str, - list_id: str, +def get_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using GraphQl. + own_attributes: bool = False, +) -> Generator["ProjectDict", None, None]: + """Get projects. Args: - project_name (str): Project name. - list_id (str): Entity list id. - fields (Optional[Iterable[str]]): Fields to fetch from server. + active (Optional[bool]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Optional[bool]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Generator[ProjectDict, None, None]: Queried projects. """ con = get_server_api_connection() - return con.get_entity_list_by_id( - project_name=project_name, - list_id=list_id, + return con.get_projects( + active=active, + library=library, fields=fields, + own_attributes=own_attributes, ) -def create_entity_list( +def get_project( project_name: str, - entity_type: "EntityListEntityType", - label: str, - *, - list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, - list_id: Optional[str] = None, -) -> str: - """Create entity list. + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["ProjectDict"]: + """Get project. Args: - project_name (str): Project name where entity list lives. - entity_type (EntityListEntityType): Which entity types can be - used in list. - label (str): Entity list label. - list_type (Optional[str]): Entity list type. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - template (Optional[dict[str, Any]]): Dynamic list template. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - items (Optional[list[dict[str, Any]]]): Initial items in - entity list. - list_id (Optional[str]): Entity list id. + project_name (str): Name of project. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[ProjectDict]: Project entity data or None + if project was not found. """ con = get_server_api_connection() - return con.create_entity_list( + return con.get_project( project_name=project_name, - entity_type=entity_type, - label=label, - list_type=list_type, - access=access, - attrib=attrib, - data=data, - tags=tags, - template=template, - owner=owner, - active=active, - items=items, - list_id=list_id, + fields=fields, + own_attributes=own_attributes, ) -def update_entity_list( +def create_project( project_name: str, - list_id: str, - *, - label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, -) -> None: - """Update entity list. - - Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id that will be updated. - label (Optional[str]): New label of entity list. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, +) -> "ProjectDict": + """Create project using AYON settings. - """ - con = get_server_api_connection() - return con.update_entity_list( - project_name=project_name, - list_id=list_id, - label=label, - access=access, - attrib=attrib, - data=data, - tags=tags, - owner=owner, - active=active, - ) + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. + Entered project name must be unique and project must not exist yet. -def delete_entity_list( - project_name: str, - list_id: str, -) -> None: - """Delete entity list from project. + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. Args: - project_name (str): Project name. - list_id (str): Entity list id that will be removed. + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. + + Returns: + ProjectDict: Created project entity. """ con = get_server_api_connection() - return con.delete_entity_list( + return con.create_project( project_name=project_name, - list_id=list_id, + project_code=project_code, + library_project=library_project, + preset_name=preset_name, ) -def get_entity_list_attribute_definitions( +def update_project( project_name: str, - list_id: str, -) -> List["EntityListAttributeDefinitionDict"]: - """Get attribute definitioins on entity list. + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes, +): + """Update project entity on server. Args: - project_name (str): Project name. - list_id (str): Entity list id. - - Returns: - List[EntityListAttributeDefinitionDict]: List of attribute + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. """ con = get_server_api_connection() - return con.get_entity_list_attribute_definitions( + return con.update_project( project_name=project_name, - list_id=list_id, + library=library, + folder_types=folder_types, + task_types=task_types, + link_types=link_types, + statuses=statuses, + tags=tags, + config=config, + attrib=attrib, + data=data, + active=active, + project_code=project_code, + **changes, ) -def set_entity_list_attribute_definitions( +def delete_project( project_name: str, - list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], -) -> None: - """Set attribute definitioins on entity list. +): + """Delete project from server. + + This will completely remove project from server without any step back. Args: - project_name (str): Project name. - list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): - List of attribute definitions. + project_name (str): Project name that will be removed. """ con = get_server_api_connection() - return con.set_entity_list_attribute_definitions( + return con.delete_project( project_name=project_name, - list_id=list_id, - attribute_definitions=attribute_definitions, ) -def create_entity_list_item( +def get_thumbnail_by_id( project_name: str, - list_id: str, - *, - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - item_id: Optional[str] = None, -) -> str: - """Create entity list item. + thumbnail_id: str, +) -> ThumbnailContent: + """Get thumbnail from server by id. + + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Item attribute values. - data (Optional[dict[str, Any]]): Item data. - tags (Optional[list[str]]): Tags of item in entity list. - item_id (Optional[str]): Id of item that will be created. + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. Returns: - str: Item id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.create_entity_list_item( + return con.get_thumbnail_by_id( project_name=project_name, - list_id=list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, - item_id=item_id, + thumbnail_id=thumbnail_id, ) -def update_entity_list_items( +def get_thumbnail( project_name: str, - list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", -) -> None: - """Update items in entity list. + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Get thumbnail from server. + + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. - mode (EntityListItemMode): Mode of items update. + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.update_entity_list_items( + return con.get_thumbnail( project_name=project_name, - list_id=list_id, - items=items, - mode=mode, + entity_type=entity_type, + entity_id=entity_id, + thumbnail_id=thumbnail_id, ) -def update_entity_list_item( +def get_folder_thumbnail( project_name: str, - list_id: str, - item_id: str, - *, - new_list_id: Optional[str], - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, -) -> None: - """Update item in entity list. + folder_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id where item lives. - item_id (str): Item id that will be removed from entity list. - new_list_id (Optional[str]): New entity list id where item will be - added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Attributes of item in entity - list. - data (Optional[dict[str, Any]]): Custom data of item in - entity list. - tags (Optional[list[str]]): Tags of item in entity list. + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.update_entity_list_item( + return con.get_folder_thumbnail( project_name=project_name, - list_id=list_id, - item_id=item_id, - new_list_id=new_list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, + folder_id=folder_id, + thumbnail_id=thumbnail_id, ) -def delete_entity_list_item( +def get_task_thumbnail( project_name: str, - list_id: str, - item_id: str, -) -> None: - """Delete item from entity list. + task_id: str, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id from which item will be removed. - item_id (str): Item id that will be removed from entity list. + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. + + Returns: + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.delete_entity_list_item( + return con.get_task_thumbnail( project_name=project_name, - list_id=list_id, - item_id=item_id, + task_id=task_id, ) -def get_rest_project( +def get_version_thumbnail( project_name: str, -) -> Optional["ProjectDict"]: - """Query project by name. - - This call returns project with anatomy data. + version_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. Args: - project_name (str): Name of project. + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_rest_project( + return con.get_version_thumbnail( project_name=project_name, + version_id=version_id, + thumbnail_id=thumbnail_id, ) -def get_rest_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, -) -> Generator["ProjectDict", None, None]: - """Query available project entities. - - User must be logged in. +def get_workfile_thumbnail( + project_name: str, + workfile_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - Generator[ProjectDict, None, None]: Available projects. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_rest_projects( - active=active, - library=library, + return con.get_workfile_thumbnail( + project_name=project_name, + workfile_id=workfile_id, + thumbnail_id=thumbnail_id, ) -def get_project_names( - active: Optional[bool] = True, - library: Optional[bool] = None, -) -> list[str]: - """Receive available project names. - - User must be logged in. +def create_thumbnail( + project_name: str, + src_filepath: str, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. Returns: - list[str]: List of available project names. + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.get_project_names( - active=active, - library=library, + return con.create_thumbnail( + project_name=project_name, + src_filepath=src_filepath, + thumbnail_id=thumbnail_id, ) -def get_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["ProjectDict", None, None]: - """Get projects. +def update_thumbnail( + project_name: str, + thumbnail_id: str, + src_filepath: str, +): + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. Args: - active (Optional[bool]): Filter active or inactive projects. - Filter is disabled when 'None' is passed. - library (Optional[bool]): Filter library projects. Filter is - disabled when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. - Returns: - Generator[ProjectDict, None, None]: Queried projects. + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.get_projects( - active=active, - library=library, - fields=fields, - own_attributes=own_attributes, + return con.update_thumbnail( + project_name=project_name, + thumbnail_id=thumbnail_id, + src_filepath=src_filepath, ) -def get_project( +def get_workfiles_info( project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + paths: Optional[Iterable[str]] = None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["ProjectDict"]: - """Get project. + own_attributes=_PLACEHOLDER, +) -> "Generator[WorkfileInfoDict, None, None]": + """Workfile info entities by passed filters. Args: - project_name (str): Name of project. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. Returns: - Optional[ProjectDict]: Project entity data or None - if project was not found. + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. """ con = get_server_api_connection() - return con.get_project( + return con.get_workfiles_info( project_name=project_name, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, + statuses=statuses, + tags=tags, + has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def create_project( +def get_workfile_info( project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, -) -> "ProjectDict": - """Create project using AYON settings. - - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by task id and workfile path. Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. - - Raises: - ValueError: When project name already exists. + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. Returns: - ProjectDict: Created project entity. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.create_project( + return con.get_workfile_info( project_name=project_name, - project_code=project_code, - library_project=library_project, - preset_name=preset_name, + task_id=task_id, + path=path, + fields=fields, + own_attributes=own_attributes, ) -def update_project( +def get_workfile_info_by_id( project_name: str, - library: Optional[bool] = None, - folder_types: Optional[list[dict[str, Any]]] = None, - task_types: Optional[list[dict[str, Any]]] = None, - link_types: Optional[list[dict[str, Any]]] = None, - statuses: Optional[list[dict[str, Any]]] = None, - tags: Optional[list[dict[str, Any]]] = None, - config: Optional[dict[str, Any]] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes, -): - """Update project entity on server. + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by id. Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.update_project( + return con.get_workfile_info_by_id( project_name=project_name, - library=library, - folder_types=folder_types, - task_types=task_types, - link_types=link_types, - statuses=statuses, - tags=tags, - config=config, - attrib=attrib, - data=data, - active=active, - project_code=project_code, - **changes, + workfile_id=workfile_id, + fields=fields, + own_attributes=own_attributes, ) -def delete_project( +def get_rest_representation( project_name: str, -): - """Delete project from server. - - This will completely remove project from server without any step back. - - Args: - project_name (str): Project name that will be removed. - - """ + representation_id: str, +) -> Optional["RepresentationDict"]: con = get_server_api_connection() - return con.delete_project( + return con.get_rest_representation( project_name=project_name, + representation_id=representation_id, ) -def get_thumbnail_by_id( +def get_representations( project_name: str, - thumbnail_id: str, -) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. + representation_ids: Optional[Iterable[str]] = None, + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + names_by_version_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["RepresentationDict", None, None]: + """Get representation entities based on passed filters from server. - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. + .. todo:: + + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + project_name (str): Name of project where to look for versions. + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (Optional[dict[str, Iterable[str]]]): Find + representations by names and version ids. This filter + discards all other filters. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + Generator[RepresentationDict, None, None]: Queried + representation entities. """ con = get_server_api_connection() - return con.get_thumbnail_by_id( + return con.get_representations( project_name=project_name, - thumbnail_id=thumbnail_id, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + names_by_version_ids=names_by_version_ids, + statuses=statuses, + tags=tags, + active=active, + has_links=has_links, + fields=fields, + own_attributes=own_attributes, ) -def get_thumbnail( +def get_representation_by_id( project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Get thumbnail from server. - - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. + representation_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["RepresentationDict"]: + """Query representation entity from server based on id filter. Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + Optional[RepresentationDict]: Queried representation + entity or None. """ con = get_server_api_connection() - return con.get_thumbnail( + return con.get_representation_by_id( project_name=project_name, - entity_type=entity_type, - entity_id=entity_id, - thumbnail_id=thumbnail_id, + representation_id=representation_id, + fields=fields, + own_attributes=own_attributes, ) -def get_folder_thumbnail( +def get_representation_by_name( project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. + representation_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["RepresentationDict"]: + """Query representation entity by name and version id. Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where to look for representation. + representation_name (str): Representation name. + version_id (str): Version id. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + Optional[RepresentationDict]: Queried representation entity + or None. """ con = get_server_api_connection() - return con.get_folder_thumbnail( + return con.get_representation_by_name( project_name=project_name, - folder_id=folder_id, - thumbnail_id=thumbnail_id, + representation_name=representation_name, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def get_task_thumbnail( +def get_representations_hierarchy( project_name: str, - task_id: str, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> dict[str, RepresentationHierarchy]: + """Find representation with parents by representation id. + + Representation entity with parent entities up to project. + + Default fields are used when any fields are set to `None`. But it is + possible to pass in empty iterable (list, set, tuple) to skip + entity. Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, RepresentationHierarchy]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_task_thumbnail( + return con.get_representations_hierarchy( project_name=project_name, - task_id=task_id, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def get_version_thumbnail( +def get_representation_hierarchy( project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> Optional[RepresentationHierarchy]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + RepresentationHierarchy: Representation hierarchy entities. """ con = get_server_api_connection() - return con.get_version_thumbnail( + return con.get_representation_hierarchy( project_name=project_name, - version_id=version_id, - thumbnail_id=thumbnail_id, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def get_workfile_thumbnail( +def get_representations_parents( project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> dict[str, RepresentationParents]: + """Find representations parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, RepresentationParents]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_workfile_thumbnail( + return con.get_representations_parents( project_name=project_name, - workfile_id=workfile_id, - thumbnail_id=thumbnail_id, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def create_thumbnail( +def get_representation_parents( project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, -) -> str: - """Create new thumbnail on server from passed path. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> Optional["RepresentationParents"]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. + RepresentationParents: Representation parent entities. """ con = get_server_api_connection() - return con.create_thumbnail( + return con.get_representation_parents( project_name=project_name, - src_filepath=src_filepath, - thumbnail_id=thumbnail_id, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def update_thumbnail( +def get_repre_ids_by_context_filters( project_name: str, - thumbnail_id: str, - src_filepath: str, -): - """Change thumbnail content by id. + context_filters: Optional[dict[str, Iterable[str]]], + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, +) -> list[str]: + """Find representation ids which match passed context filters. - Update can be also used to create new thumbnail. + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or ``re.Pattern`` can + be used. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. - Raises: - ValueError: When thumbnail source cannot be processed. + Returns: + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> from ayon_api import get_repre_ids_by_context_filters + >>> from ayon_api import get_representations + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "product": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) """ con = get_server_api_connection() - return con.update_thumbnail( + return con.get_repre_ids_by_context_filters( project_name=project_name, - thumbnail_id=thumbnail_id, - src_filepath=src_filepath, + context_filters=context_filters, + representation_names=representation_names, + version_ids=version_ids, ) -def get_workfiles_info( +def create_representation( project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - paths: Optional[Iterable[str]] = None, - path_regex: Optional[str] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> "Generator[WorkfileInfoDict, None, None]": - """Workfile info entities by passed filters. + name: str, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + representation_id: Optional[str] = None, +) -> str: + """Create new representation. Args: - project_name (str): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used - for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used - for filtering. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + project_name (str): Project name. + name (str): Representation name. + version_id (str): Parent version id. + files (Optional[list[dict]]): Representation files information. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + representation_id (Optional[str]): Representation id. If not + passed new id is generated. Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. + str: Representation id. """ con = get_server_api_connection() - return con.get_workfiles_info( + return con.create_representation( project_name=project_name, - workfile_ids=workfile_ids, - task_ids=task_ids, - paths=paths, - path_regex=path_regex, - statuses=statuses, + name=name, + version_id=version_id, + files=files, + attrib=attrib, + data=data, + traits=traits, tags=tags, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, + status=status, + active=active, + representation_id=representation_id, ) -def get_workfile_info( +def update_representation( project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. + representation_id: str, + name: Optional[str] = None, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, +): + """Update representation entity on server. - Args: - project_name (str): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + Update of ``data`` will override existing value on folder entity. - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + representation_id (str): Representation id. + name (Optional[str]): New name. + version_id (Optional[str]): New version id. + files (Optional[list[dict]]): New files + information. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + traits (Optional[dict[str, Any]]): New traits. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. """ con = get_server_api_connection() - return con.get_workfile_info( + return con.update_representation( project_name=project_name, - task_id=task_id, - path=path, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, + name=name, + version_id=version_id, + files=files, + attrib=attrib, + data=data, + traits=traits, + tags=tags, + status=status, + active=active, ) -def get_workfile_info_by_id( +def delete_representation( project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. + representation_id: str, +): + """Delete representation. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + project_name (str): Project name. + representation_id (str): Representation id to delete. """ con = get_server_api_connection() - return con.get_workfile_info_by_id( + return con.delete_representation( project_name=project_name, - workfile_id=workfile_id, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, ) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index d98869df0..95021aec7 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -1,14 +1,18 @@ from __future__ import annotations import typing -from typing import Optional, Any +from typing import Optional, Any, Iterable import requests from .utils import TransferProgress, RequestType if typing.TYPE_CHECKING: - from .typing import AnyEntityDict, ServerVersion + from .typing import ( + AnyEntityDict, + ServerVersion, + ProjectDict, + ) _PLACEHOLDER = object() @@ -89,6 +93,14 @@ def get_rest_entity_by_id( ) -> Optional["AnyEntityDict"]: raise NotImplementedError() + def get_project( + self, + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional["ProjectDict"]: + raise NotImplementedError() + def _prepare_fields( self, entity_type: str, diff --git a/ayon_api/_representations.py b/ayon_api/_representations.py new file mode 100644 index 000000000..a1d7d2ad1 --- /dev/null +++ b/ayon_api/_representations.py @@ -0,0 +1,746 @@ +from __future__ import annotations + +import json +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ._base import _BaseServerAPI, _PLACEHOLDER +from .constants import REPRESENTATION_FILES_FIELDS +from .utils import ( + RepresentationHierarchy, + RepresentationParents, + PatternType, + create_entity_id, +) +from .graphql_queries import ( + representations_graphql_query, + representations_hierarchy_qraphql_query, +) + +if typing.TYPE_CHECKING: + from .typing import RepresentationDict + + +class _RepresentationsAPI(_BaseServerAPI): + def get_rest_representation( + self, project_name: str, representation_id: str + ) -> Optional["RepresentationDict"]: + return self.get_rest_entity_by_id( + project_name, "representation", representation_id + ) + + def get_representations( + self, + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + names_by_version_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator["RepresentationDict", None, None]: + """Get representation entities based on passed filters from server. + + .. todo:: + + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. + + Args: + project_name (str): Name of project where to look for versions. + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (Optional[dict[str, Iterable[str]]]): Find + representations by names and version ids. This filter + discards all other filters. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Generator[RepresentationDict, None, None]: Queried + representation entities. + + """ + if not fields: + fields = self.get_default_fields_for_type("representation") + else: + fields = set(fields) + self._prepare_fields("representation", fields) + + if active is not None: + fields.add("active") + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for representations. " + "The argument will be removed form function signature in " + "future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + if "files" in fields: + fields.discard("files") + fields |= REPRESENTATION_FILES_FIELDS + + filters = { + "projectName": project_name + } + + if representation_ids is not None: + representation_ids = set(representation_ids) + if not representation_ids: + return + filters["representationIds"] = list(representation_ids) + + version_ids_filter = None + representation_names_filter = None + if names_by_version_ids is not None: + version_ids_filter = set() + representation_names_filter = set() + for version_id, names in names_by_version_ids.items(): + version_ids_filter.add(version_id) + representation_names_filter |= set(names) + + if not version_ids_filter or not representation_names_filter: + return + + else: + if representation_names is not None: + representation_names_filter = set(representation_names) + if not representation_names_filter: + return + + if version_ids is not None: + version_ids_filter = set(version_ids) + if not version_ids_filter: + return + + if version_ids_filter: + filters["versionIds"] = list(version_ids_filter) + + if representation_names_filter: + filters["representationNames"] = list(representation_names_filter) + + if statuses is not None: + statuses = set(statuses) + if not statuses: + return + filters["representationStatuses"] = list(statuses) + + if tags is not None: + tags = set(tags) + if not tags: + return + filters["representationTags"] = list(tags) + + if has_links is not None: + filters["representationHasLinks"] = has_links.upper() + + query = representations_graphql_query(fields) + + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for repre in parsed_data["project"]["representations"]: + if active is not None and active is not repre["active"]: + continue + + self._convert_entity_data(repre) + + self._representation_conversion(repre) + + yield repre + + def get_representation_by_id( + self, + project_name: str, + representation_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional["RepresentationDict"]: + """Query representation entity from server based on id filter. + + Args: + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[RepresentationDict]: Queried representation + entity or None. + + """ + representations = self.get_representations( + project_name, + representation_ids=[representation_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for representation in representations: + return representation + return None + + def get_representation_by_name( + self, + project_name: str, + representation_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional["RepresentationDict"]: + """Query representation entity by name and version id. + + Args: + project_name (str): Project where to look for representation. + representation_name (str): Representation name. + version_id (str): Version id. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[RepresentationDict]: Queried representation entity + or None. + + """ + representations = self.get_representations( + project_name, + representation_names=[representation_name], + version_ids=[version_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for representation in representations: + return representation + return None + + def get_representations_hierarchy( + self, + project_name: str, + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, + ) -> dict[str, RepresentationHierarchy]: + """Find representation with parents by representation id. + + Representation entity with parent entities up to project. + + Default fields are used when any fields are set to `None`. But it is + possible to pass in empty iterable (list, set, tuple) to skip + entity. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. + + Returns: + dict[str, RepresentationHierarchy]: Parent entities by + representation id. + + """ + if not representation_ids: + return {} + + if project_fields is not None: + project_fields = set(project_fields) + self._prepare_fields("project", project_fields) + + project = {} + if project_fields is None: + project = self.get_project(project_name) + + elif project_fields: + # Keep project as empty dictionary if does not have + # filled any fields + project = self.get_project( + project_name, fields=project_fields + ) + + repre_ids = set(representation_ids) + output = { + repre_id: RepresentationHierarchy( + project, None, None, None, None, None + ) + for repre_id in representation_ids + } + + if folder_fields is None: + folder_fields = self.get_default_fields_for_type("folder") + else: + folder_fields = set(folder_fields) + + if task_fields is None: + task_fields = self.get_default_fields_for_type("task") + else: + task_fields = set(task_fields) + + if product_fields is None: + product_fields = self.get_default_fields_for_type("product") + else: + product_fields = set(product_fields) + + if version_fields is None: + version_fields = self.get_default_fields_for_type("version") + else: + version_fields = set(version_fields) + + if representation_fields is None: + representation_fields = self.get_default_fields_for_type( + "representation" + ) + else: + representation_fields = set(representation_fields) + + for (entity_type, fields) in ( + ("folder", folder_fields), + ("task", task_fields), + ("product", product_fields), + ("version", version_fields), + ("representation", representation_fields), + ): + self._prepare_fields(entity_type, fields) + + representation_fields.add("id") + + query = representations_hierarchy_qraphql_query( + folder_fields, + task_fields, + product_fields, + version_fields, + representation_fields, + ) + query.set_variable_value("projectName", project_name) + query.set_variable_value("representationIds", list(repre_ids)) + + parsed_data = query.query(self) + for repre in parsed_data["project"]["representations"]: + repre_id = repre["id"] + version = repre.pop("version", {}) + product = version.pop("product", {}) + task = version.pop("task", None) + folder = product.pop("folder", {}) + self._convert_entity_data(repre) + self._representation_conversion(repre) + self._convert_entity_data(version) + self._convert_entity_data(product) + self._convert_entity_data(folder) + if task: + self._convert_entity_data(task) + + output[repre_id] = RepresentationHierarchy( + project, folder, task, product, version, repre + ) + + return output + + def get_representation_hierarchy( + self, + project_name: str, + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, + ) -> Optional[RepresentationHierarchy]: + """Find representation parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. + + Returns: + RepresentationHierarchy: Representation hierarchy entities. + + """ + if not representation_id: + return None + + parents_by_repre_id = self.get_representations_hierarchy( + project_name, + [representation_id], + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, + ) + return parents_by_repre_id[representation_id] + + def get_representations_parents( + self, + project_name: str, + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + ) -> dict[str, RepresentationParents]: + """Find representations parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + + Returns: + dict[str, RepresentationParents]: Parent entities by + representation id. + + """ + hierarchy_by_repre_id = self.get_representations_hierarchy( + project_name, + representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=set(), + product_fields=product_fields, + version_fields=version_fields, + representation_fields={"id"}, + ) + return { + repre_id: RepresentationParents( + hierarchy.version, + hierarchy.product, + hierarchy.folder, + hierarchy.project, + ) + for repre_id, hierarchy in hierarchy_by_repre_id.items() + } + + def get_representation_parents( + self, + project_name: str, + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + ) -> Optional["RepresentationParents"]: + """Find representation parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + + Returns: + RepresentationParents: Representation parent entities. + + """ + if not representation_id: + return None + + parents_by_repre_id = self.get_representations_parents( + project_name, + [representation_id], + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, + ) + return parents_by_repre_id[representation_id] + + def get_repre_ids_by_context_filters( + self, + project_name: str, + context_filters: Optional[dict[str, Iterable[str]]], + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + ) -> list[str]: + """Find representation ids which match passed context filters. + + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or ``re.Pattern`` can + be used. + + Args: + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. + + Returns: + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> from ayon_api import get_repre_ids_by_context_filters + >>> from ayon_api import get_representations + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "product": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) + + """ + if not isinstance(context_filters, dict): + raise TypeError( + f"Expected 'dict' got {str(type(context_filters))}" + ) + + filter_body = {} + if representation_names is not None: + if not representation_names: + return [] + filter_body["names"] = list(set(representation_names)) + + if version_ids is not None: + if not version_ids: + return [] + filter_body["versionIds"] = list(set(version_ids)) + + body_context_filters = [] + for key, filters in context_filters.items(): + if not isinstance(filters, (set, list, tuple)): + raise TypeError( + "Expected 'set', 'list', 'tuple' got {}".format( + str(type(filters)))) + + new_filters = set() + for filter_value in filters: + if isinstance(filter_value, PatternType): + filter_value = filter_value.pattern + new_filters.add(filter_value) + + body_context_filters.append({ + "key": key, + "values": list(new_filters) + }) + + response = self.post( + f"projects/{project_name}/repreContextFilter", + context=body_context_filters, + **filter_body + ) + response.raise_for_status() + return response.data["ids"] + + def create_representation( + self, + project_name: str, + name: str, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]]=None, + status: Optional[str] = None, + active: Optional[bool] = None, + representation_id: Optional[str] = None, + ) -> str: + """Create new representation. + + Args: + project_name (str): Project name. + name (str): Representation name. + version_id (str): Parent version id. + files (Optional[list[dict]]): Representation files information. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + representation_id (Optional[str]): Representation id. If not + passed new id is generated. + + Returns: + str: Representation id. + + """ + if not representation_id: + representation_id = create_entity_id() + create_data = { + "id": representation_id, + "name": name, + "versionId": version_id, + } + for key, value in ( + ("files", files), + ("attrib", attrib), + ("data", data), + ("traits", traits), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/representations", + **create_data + ) + response.raise_for_status() + return representation_id + + def update_representation( + self, + project_name: str, + representation_id: str, + name: Optional[str] = None, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ): + """Update representation entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + representation_id (str): Representation id. + name (Optional[str]): New name. + version_id (Optional[str]): New version id. + files (Optional[list[dict]]): New files + information. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + traits (Optional[dict[str, Any]]): New traits. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("versionId", version_id), + ("files", files), + ("attrib", attrib), + ("data", data), + ("traits", traits), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/representations/{representation_id}", + **update_data + ) + response.raise_for_status() + + def delete_representation( + self, project_name: str, representation_id: str + ): + """Delete representation. + + Args: + project_name (str): Project name. + representation_id (str): Representation id to delete. + + """ + response = self.delete( + f"projects/{project_name}/representations/{representation_id}" + ) + response.raise_for_status() + + def _representation_conversion( + self, representation: "RepresentationDict" + ): + if "context" in representation: + orig_context = representation["context"] + context = {} + if orig_context and orig_context != "null": + context = json.loads(orig_context) + representation["context"] = context + + repre_files = representation.get("files") + if not repre_files: + return + + for repre_file in repre_files: + repre_file_size = repre_file.get("size") + if repre_file_size is not None: + repre_file["size"] = int(repre_file["size"]) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b2d58d47f..4a2767255 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -80,7 +80,6 @@ get_machine_name, fill_own_attribs, prepare_list_filters, - PatternType, ) from ._base import _PLACEHOLDER from ._actions import _ActionsAPI @@ -93,6 +92,7 @@ from ._projects import _ProjectsAPI from ._thumbnails import _ThumbnailsAPI from ._workfiles import _WorkfilesAPI +from ._representations import _RepresentationsAPI if typing.TYPE_CHECKING: from typing import Union @@ -237,12 +237,13 @@ class ServerAPI( _ActivitiesAPI, _AddonsAPI, _EventsAPI, + _ProjectsAPI, _FoldersAPI, + _RepresentationsAPI, + _WorkfilesAPI, _LinksAPI, _ListsAPI, - _ProjectsAPI, _ThumbnailsAPI, - _WorkfilesAPI, ): """Base handler of connection to server. @@ -3439,13 +3440,6 @@ def get_rest_version( ) -> Optional["VersionDict"]: return self.get_rest_entity_by_id(project_name, "version", version_id) - def get_rest_representation( - self, project_name: str, representation_id: str - ) -> Optional["RepresentationDict"]: - return self.get_rest_entity_by_id( - project_name, "representation", representation_id - ) - def get_tasks( self, project_name: str, @@ -5024,719 +5018,6 @@ def delete_version(self, project_name: str, version_id: str): ) response.raise_for_status() - def _representation_conversion( - self, representation: "RepresentationDict" - ): - if "context" in representation: - orig_context = representation["context"] - context = {} - if orig_context and orig_context != "null": - context = json.loads(orig_context) - representation["context"] = context - - repre_files = representation.get("files") - if not repre_files: - return - - for repre_file in repre_files: - repre_file_size = repre_file.get("size") - if repre_file_size is not None: - repre_file["size"] = int(repre_file["size"]) - - def get_representations( - self, - project_name: str, - representation_ids: Optional[Iterable[str]] = None, - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - names_by_version_ids: Optional[Dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Generator["RepresentationDict", None, None]: - """Get representation entities based on passed filters from server. - - .. todo:: - - Add separated function for 'names_by_version_ids' filtering. - Because can't be combined with others. - - Args: - project_name (str): Name of project where to look for versions. - representation_ids (Optional[Iterable[str]]): Representation ids - used for representation filtering. - representation_names (Optional[Iterable[str]]): Representation - names used for representation filtering. - version_ids (Optional[Iterable[str]]): Version ids used for - representation filtering. Versions are parents of - representations. - names_by_version_ids (Optional[Dict[str, Iterable[str]]]): Find - representations by names and version ids. This filter - discards all other filters. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Generator[RepresentationDict, None, None]: Queried - representation entities. - - """ - if not fields: - fields = self.get_default_fields_for_type("representation") - else: - fields = set(fields) - self._prepare_fields("representation", fields) - - if active is not None: - fields.add("active") - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for representations. " - "The argument will be removed form function signature in " - "future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - if "files" in fields: - fields.discard("files") - fields |= REPRESENTATION_FILES_FIELDS - - filters = { - "projectName": project_name - } - - if representation_ids is not None: - representation_ids = set(representation_ids) - if not representation_ids: - return - filters["representationIds"] = list(representation_ids) - - version_ids_filter = None - representation_names_filter = None - if names_by_version_ids is not None: - version_ids_filter = set() - representation_names_filter = set() - for version_id, names in names_by_version_ids.items(): - version_ids_filter.add(version_id) - representation_names_filter |= set(names) - - if not version_ids_filter or not representation_names_filter: - return - - else: - if representation_names is not None: - representation_names_filter = set(representation_names) - if not representation_names_filter: - return - - if version_ids is not None: - version_ids_filter = set(version_ids) - if not version_ids_filter: - return - - if version_ids_filter: - filters["versionIds"] = list(version_ids_filter) - - if representation_names_filter: - filters["representationNames"] = list(representation_names_filter) - - if statuses is not None: - statuses = set(statuses) - if not statuses: - return - filters["representationStatuses"] = list(statuses) - - if tags is not None: - tags = set(tags) - if not tags: - return - filters["representationTags"] = list(tags) - - if has_links is not None: - filters["representationHasLinks"] = has_links.upper() - - query = representations_graphql_query(fields) - - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for repre in parsed_data["project"]["representations"]: - if active is not None and active is not repre["active"]: - continue - - self._convert_entity_data(repre) - - self._representation_conversion(repre) - - yield repre - - def get_representation_by_id( - self, - project_name: str, - representation_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["RepresentationDict"]: - """Query representation entity from server based on id filter. - - Args: - project_name (str): Project where to look for representation. - representation_id (str): Id of representation. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[RepresentationDict]: Queried representation - entity or None. - - """ - representations = self.get_representations( - project_name, - representation_ids=[representation_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for representation in representations: - return representation - return None - - def get_representation_by_name( - self, - project_name: str, - representation_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["RepresentationDict"]: - """Query representation entity by name and version id. - - Args: - project_name (str): Project where to look for representation. - representation_name (str): Representation name. - version_id (str): Version id. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[RepresentationDict]: Queried representation entity - or None. - - """ - representations = self.get_representations( - project_name, - representation_names=[representation_name], - version_ids=[version_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for representation in representations: - return representation - return None - - def get_representations_hierarchy( - self, - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, - ) -> Dict[str, RepresentationHierarchy]: - """Find representation with parents by representation id. - - Representation entity with parent entities up to project. - - Default fields are used when any fields are set to `None`. But it is - possible to pass in empty iterable (list, set, tuple) to skip - entity. - - Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. - - Returns: - dict[str, RepresentationHierarchy]: Parent entities by - representation id. - - """ - if not representation_ids: - return {} - - if project_fields is not None: - project_fields = set(project_fields) - self._prepare_fields("project", project_fields) - - project = {} - if project_fields is None: - project = self.get_project(project_name) - - elif project_fields: - # Keep project as empty dictionary if does not have - # filled any fields - project = self.get_project( - project_name, fields=project_fields - ) - - repre_ids = set(representation_ids) - output = { - repre_id: RepresentationHierarchy( - project, None, None, None, None, None - ) - for repre_id in representation_ids - } - - if folder_fields is None: - folder_fields = self.get_default_fields_for_type("folder") - else: - folder_fields = set(folder_fields) - - if task_fields is None: - task_fields = self.get_default_fields_for_type("task") - else: - task_fields = set(task_fields) - - if product_fields is None: - product_fields = self.get_default_fields_for_type("product") - else: - product_fields = set(product_fields) - - if version_fields is None: - version_fields = self.get_default_fields_for_type("version") - else: - version_fields = set(version_fields) - - if representation_fields is None: - representation_fields = self.get_default_fields_for_type( - "representation" - ) - else: - representation_fields = set(representation_fields) - - for (entity_type, fields) in ( - ("folder", folder_fields), - ("task", task_fields), - ("product", product_fields), - ("version", version_fields), - ("representation", representation_fields), - ): - self._prepare_fields(entity_type, fields) - - representation_fields.add("id") - - query = representations_hierarchy_qraphql_query( - folder_fields, - task_fields, - product_fields, - version_fields, - representation_fields, - ) - query.set_variable_value("projectName", project_name) - query.set_variable_value("representationIds", list(repre_ids)) - - parsed_data = query.query(self) - for repre in parsed_data["project"]["representations"]: - repre_id = repre["id"] - version = repre.pop("version", {}) - product = version.pop("product", {}) - task = version.pop("task", None) - folder = product.pop("folder", {}) - self._convert_entity_data(repre) - self._representation_conversion(repre) - self._convert_entity_data(version) - self._convert_entity_data(product) - self._convert_entity_data(folder) - if task: - self._convert_entity_data(task) - - output[repre_id] = RepresentationHierarchy( - project, folder, task, product, version, repre - ) - - return output - - def get_representation_hierarchy( - self, - project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, - ) -> Optional[RepresentationHierarchy]: - """Find representation parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. - - Returns: - RepresentationHierarchy: Representation hierarchy entities. - - """ - if not representation_id: - return None - - parents_by_repre_id = self.get_representations_hierarchy( - project_name, - [representation_id], - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, - ) - return parents_by_repre_id[representation_id] - - def get_representations_parents( - self, - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - ) -> Dict[str, RepresentationParents]: - """Find representations parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - - Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. - - """ - hierarchy_by_repre_id = self.get_representations_hierarchy( - project_name, - representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=set(), - product_fields=product_fields, - version_fields=version_fields, - representation_fields={"id"}, - ) - return { - repre_id: RepresentationParents( - hierarchy.version, - hierarchy.product, - hierarchy.folder, - hierarchy.project, - ) - for repre_id, hierarchy in hierarchy_by_repre_id.items() - } - - def get_representation_parents( - self, - project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - ) -> Optional["RepresentationParents"]: - """Find representation parents by representation id. - - Representation parent entities up to project. - - Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - - Returns: - RepresentationParents: Representation parent entities. - - """ - if not representation_id: - return None - - parents_by_repre_id = self.get_representations_parents( - project_name, - [representation_id], - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, - ) - return parents_by_repre_id[representation_id] - - def get_repre_ids_by_context_filters( - self, - project_name: str, - context_filters: Optional[Dict[str, Iterable[str]]], - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - ) -> List[str]: - """Find representation ids which match passed context filters. - - Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or - product name, product type and many more. This implementation gives - option to quickly filter representation based on representation data - in database. - - Context filters have defined structure. To define filter of nested - subfield use dot '.' as delimiter (For example 'task.name'). - Filter values can be regex filters. String or ``re.Pattern`` can - be used. - - Args: - project_name (str): Project where to look for representations. - context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Optional[Iterable[str]]): Representation - names, can be used as additional filter for representations - by their names. - version_ids (Optional[Iterable[str]]): Version ids, can be used - as additional filter for representations by their parent ids. - - Returns: - list[str]: Representation ids that match passed filters. - - Example: - The function returns just representation ids so if entities are - required for funtionality they must be queried afterwards by - their ids. - >>> project_name = "testProject" - >>> filters = { - ... "task.name": ["[aA]nimation"], - ... "product": [".*[Mm]ain"] - ... } - >>> repre_ids = get_repre_ids_by_context_filters( - ... project_name, filters) - >>> repres = get_representations(project_name, repre_ids) - - """ - if not isinstance(context_filters, dict): - raise TypeError( - f"Expected 'dict' got {str(type(context_filters))}" - ) - - filter_body = {} - if representation_names is not None: - if not representation_names: - return [] - filter_body["names"] = list(set(representation_names)) - - if version_ids is not None: - if not version_ids: - return [] - filter_body["versionIds"] = list(set(version_ids)) - - body_context_filters = [] - for key, filters in context_filters.items(): - if not isinstance(filters, (set, list, tuple)): - raise TypeError( - "Expected 'set', 'list', 'tuple' got {}".format( - str(type(filters)))) - - new_filters = set() - for filter_value in filters: - if isinstance(filter_value, PatternType): - filter_value = filter_value.pattern - new_filters.add(filter_value) - - body_context_filters.append({ - "key": key, - "values": list(new_filters) - }) - - response = self.post( - f"projects/{project_name}/repreContextFilter", - context=body_context_filters, - **filter_body - ) - response.raise_for_status() - return response.data["ids"] - - def create_representation( - self, - project_name: str, - name: str, - version_id: str, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]]=None, - status: Optional[str] = None, - active: Optional[bool] = None, - representation_id: Optional[str] = None, - ) -> str: - """Create new representation. - - Args: - project_name (str): Project name. - name (str): Representation name. - version_id (str): Parent version id. - files (Optional[list[dict]]): Representation files information. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - representation_id (Optional[str]): Representation id. If not - passed new id is generated. - - Returns: - str: Representation id. - - """ - if not representation_id: - representation_id = create_entity_id() - create_data = { - "id": representation_id, - "name": name, - "versionId": version_id, - } - for key, value in ( - ("files", files), - ("attrib", attrib), - ("data", data), - ("traits", traits), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/representations", - **create_data - ) - response.raise_for_status() - return representation_id - - def update_representation( - self, - project_name: str, - representation_id: str, - name: Optional[str] = None, - version_id: Optional[str] = None, - files: Optional[List[Dict[str, Any]]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - traits: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - ): - """Update representation entity on server. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - representation_id (str): Representation id. - name (Optional[str]): New name. - version_id (Optional[str]): New version id. - files (Optional[list[dict]]): New files - information. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - traits (Optional[dict[str, Any]]): New traits. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("versionId", version_id), - ("files", files), - ("attrib", attrib), - ("data", data), - ("traits", traits), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/representations/{representation_id}", - **update_data - ) - response.raise_for_status() - - def delete_representation( - self, project_name: str, representation_id: str - ): - """Delete representation. - - Args: - project_name (str): Project name. - representation_id (str): Representation id to delete. - - """ - response = self.delete( - f"projects/{project_name}/representations/{representation_id}" - ) - response.raise_for_status() - # --- Batch operations processing --- def send_batch_operations( self, From 11ba7d881e9b436eb5e240cb67523d6338e6acd8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 19:51:36 +0200 Subject: [PATCH 138/506] moved tasks, products and versions --- automated_api.py | 8 +- ayon_api/__init__.py | 184 +- ayon_api/_api.py | 4842 ++++++++++++++++++++-------------------- ayon_api/_products.py | 501 +++++ ayon_api/_tasks.py | 514 +++++ ayon_api/_versions.py | 639 ++++++ ayon_api/server_api.py | 1620 +------------- 7 files changed, 4184 insertions(+), 4124 deletions(-) create mode 100644 ayon_api/_products.py create mode 100644 ayon_api/_tasks.py create mode 100644 ayon_api/_versions.py diff --git a/automated_api.py b/automated_api.py index a581dde58..e9804adfd 100644 --- a/automated_api.py +++ b/automated_api.py @@ -340,6 +340,9 @@ def prepare_api_functions(api_globals): _AddonsAPI, _EventsAPI, _FoldersAPI, + _TasksAPI, + _ProductsAPI, + _VersionsAPI, _LinksAPI, _ListsAPI, _ProjectsAPI, @@ -354,10 +357,13 @@ def prepare_api_functions(api_globals): _items.extend(_ActivitiesAPI.__dict__.items()) _items.extend(_AddonsAPI.__dict__.items()) _items.extend(_EventsAPI.__dict__.items()) - _items.extend(_FoldersAPI.__dict__.items()) _items.extend(_LinksAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) _items.extend(_ProjectsAPI.__dict__.items()) + _items.extend(_FoldersAPI.__dict__.items()) + _items.extend(_TasksAPI.__dict__.items()) + _items.extend(_ProductsAPI.__dict__.items()) + _items.extend(_VersionsAPI.__dict__.items()) _items.extend(_ThumbnailsAPI.__dict__.items()) _items.extend(_WorkfilesAPI.__dict__.items()) _items.extend(_RepresentationsAPI.__dict__.items()) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index dd7b1a053..a12e27624 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -127,40 +127,6 @@ save_secret, delete_secret, get_rest_entity_by_id, - get_rest_task, - get_rest_product, - get_rest_version, - get_tasks, - get_task_by_name, - get_task_by_id, - get_tasks_by_folder_paths, - get_tasks_by_folder_path, - get_task_by_folder_path, - create_task, - update_task, - delete_task, - get_products, - get_product_by_id, - get_product_by_name, - get_product_types, - get_project_product_types, - get_product_type_names, - create_product, - update_product, - delete_product, - get_versions, - get_version_by_id, - get_version_by_name, - get_hero_version_by_id, - get_hero_version_by_product_id, - get_hero_versions, - get_last_versions, - get_last_version_by_product_id, - get_last_version_by_product_name, - version_is_latest, - create_version, - update_version, - delete_version, send_batch_operations, get_actions, trigger_action, @@ -187,18 +153,6 @@ dispatch_event, delete_event, enroll_event_job, - get_rest_folder, - get_rest_folders, - get_folders_hierarchy, - get_folders_rest, - get_folders, - get_folder_by_id, - get_folder_by_path, - get_folder_by_name, - get_folder_ids_with_products, - create_folder, - update_folder, - delete_folder, get_full_link_type_name, get_link_types, get_link_type, @@ -238,6 +192,52 @@ create_project, update_project, delete_project, + get_rest_folder, + get_rest_folders, + get_folders_hierarchy, + get_folders_rest, + get_folders, + get_folder_by_id, + get_folder_by_path, + get_folder_by_name, + get_folder_ids_with_products, + create_folder, + update_folder, + delete_folder, + get_rest_task, + get_tasks, + get_task_by_name, + get_task_by_id, + get_tasks_by_folder_paths, + get_tasks_by_folder_path, + get_task_by_folder_path, + create_task, + update_task, + delete_task, + get_rest_product, + get_products, + get_product_by_id, + get_product_by_name, + get_product_types, + get_project_product_types, + get_product_type_names, + create_product, + update_product, + delete_product, + get_rest_version, + get_versions, + get_version_by_id, + get_version_by_name, + get_hero_version_by_id, + get_hero_version_by_product_id, + get_hero_versions, + get_last_versions, + get_last_version_by_product_id, + get_last_version_by_product_name, + version_is_latest, + create_version, + update_version, + delete_version, get_thumbnail_by_id, get_thumbnail, get_folder_thumbnail, @@ -391,40 +391,6 @@ "save_secret", "delete_secret", "get_rest_entity_by_id", - "get_rest_task", - "get_rest_product", - "get_rest_version", - "get_tasks", - "get_task_by_name", - "get_task_by_id", - "get_tasks_by_folder_paths", - "get_tasks_by_folder_path", - "get_task_by_folder_path", - "create_task", - "update_task", - "delete_task", - "get_products", - "get_product_by_id", - "get_product_by_name", - "get_product_types", - "get_project_product_types", - "get_product_type_names", - "create_product", - "update_product", - "delete_product", - "get_versions", - "get_version_by_id", - "get_version_by_name", - "get_hero_version_by_id", - "get_hero_version_by_product_id", - "get_hero_versions", - "get_last_versions", - "get_last_version_by_product_id", - "get_last_version_by_product_name", - "version_is_latest", - "create_version", - "update_version", - "delete_version", "send_batch_operations", "get_actions", "trigger_action", @@ -451,18 +417,6 @@ "dispatch_event", "delete_event", "enroll_event_job", - "get_rest_folder", - "get_rest_folders", - "get_folders_hierarchy", - "get_folders_rest", - "get_folders", - "get_folder_by_id", - "get_folder_by_path", - "get_folder_by_name", - "get_folder_ids_with_products", - "create_folder", - "update_folder", - "delete_folder", "get_full_link_type_name", "get_link_types", "get_link_type", @@ -502,6 +456,52 @@ "create_project", "update_project", "delete_project", + "get_rest_folder", + "get_rest_folders", + "get_folders_hierarchy", + "get_folders_rest", + "get_folders", + "get_folder_by_id", + "get_folder_by_path", + "get_folder_by_name", + "get_folder_ids_with_products", + "create_folder", + "update_folder", + "delete_folder", + "get_rest_task", + "get_tasks", + "get_task_by_name", + "get_task_by_id", + "get_tasks_by_folder_paths", + "get_tasks_by_folder_path", + "get_task_by_folder_path", + "create_task", + "update_task", + "delete_task", + "get_rest_product", + "get_products", + "get_product_by_id", + "get_product_by_name", + "get_product_types", + "get_project_product_types", + "get_product_type_names", + "create_product", + "update_product", + "delete_product", + "get_rest_version", + "get_versions", + "get_version_by_id", + "get_version_by_name", + "get_hero_version_by_id", + "get_hero_version_by_product_id", + "get_hero_versions", + "get_last_versions", + "get_last_version_by_product_id", + "get_last_version_by_product_name", + "version_is_latest", + "create_version", + "update_version", + "delete_version", "get_thumbnail_by_id", "get_thumbnail", "get_folder_thumbnail", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 6df3bebb3..8ae32db4d 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -2580,2208 +2580,2169 @@ def get_rest_entity_by_id( ) -def get_rest_task( +def send_batch_operations( project_name: str, - task_id: str, -) -> Optional["TaskDict"]: + operations: List[Dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> List[Dict[str, Any]]: + """Post multiple CRUD 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.get_rest_task( + return con.send_batch_operations( project_name=project_name, - task_id=task_id, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_rest_product( - project_name: str, - product_id: str, -) -> Optional["ProductDict"]: +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> List["ActionManifestDict"]: + """Get actions for a context. + + Args: + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. + + Returns: + List[ActionManifestDict]: List of action manifests. + + """ con = get_server_api_connection() - return con.get_rest_product( + return con.get_actions( project_name=project_name, - product_id=product_id, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, ) -def get_rest_version( - project_name: str, - version_id: str, -) -> Optional["VersionDict"]: +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + """ con = get_server_api_connection() - return con.get_rest_version( + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - version_id=version_id, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_tasks( +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: Action configuration data. + + """ + con = get_server_api_connection() + return con.get_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) + + +def set_action_config( + identifier: str, + addon_name: str, + addon_version: str, + value: Dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[List[str]] = None, + entity_subtypes: Optional[List[str]] = None, + form_data: Optional[Dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Set action configuration. + + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[Dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[List[str]]): List of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[List[str]]): List of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[Dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + + Returns: + ActionConfigResponse: New action configuration data. + + """ + con = get_server_api_connection() + return con.set_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + value=value, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) + + +def take_action( + action_token: str, +) -> "ActionTakeResponse": + """Take action metadata using an action token. + + Args: + action_token (str): AYON launcher action token. + + Returns: + ActionTakeResponse: Action metadata describing how to launch + action. + + """ + con = get_server_api_connection() + return con.take_action( + action_token=action_token, + ) + + +def abort_action( + action_token: str, + message: Optional[str] = None, +) -> None: + """Abort action using an action token. + + Args: + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. + + """ + con = get_server_api_connection() + return con.abort_action( + action_token=action_token, + message=message, + ) + + +def get_activities( project_name: str, - task_ids: Optional[Iterable[str]] = None, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, + 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["ActivityReferenceType"]] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["TaskDict", None, None]: - """Query task entities from server. + limit: Optional[int] = None, + order: Optional[SortOrder] = None, +) -> Generator[dict[str, Any], None, None]: + """Get activities from server with filtering options. Args: - project_name (str): Name of project. - task_ids (Iterable[str]): Task ids to filter. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + 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[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending + or descending order. It is recommended to set 'limit' + when used descending. Returns: - Generator[TaskDict, None, None]: Queried task entities. + Generator[dict[str, Any]]: Available activities matching filters. """ con = get_server_api_connection() - return con.get_tasks( + return con.get_activities( project_name=project_name, - task_ids=task_ids, - task_names=task_names, - task_types=task_types, - folder_ids=folder_ids, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, + activity_ids=activity_ids, + activity_types=activity_types, + entity_ids=entity_ids, + entity_names=entity_names, + entity_type=entity_type, + changed_after=changed_after, + changed_before=changed_before, + reference_types=reference_types, fields=fields, - own_attributes=own_attributes, + limit=limit, + order=order, ) -def get_task_by_name( +def get_activity_by_id( project_name: str, - folder_id: str, - task_name: str, + activity_id: str, + reference_types: Optional[Iterable["ActivityReferenceType"]] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by name and folder id. +) -> Optional[dict[str, Any]]: + """Get activity by id. Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - task_name (str): Task name - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[dict[str, Any]]: Activity data or None if activity is not + found. """ con = get_server_api_connection() - return con.get_task_by_name( + return con.get_activity_by_id( project_name=project_name, - folder_id=folder_id, - task_name=task_name, + activity_id=activity_id, + reference_types=reference_types, fields=fields, - own_attributes=own_attributes, ) -def get_task_by_id( +def create_activity( project_name: str, - task_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by id. + 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, +) -> str: + """Create activity on a project. Args: - project_name (str): Name of project where to look for queried - entities. - task_id (str): Task id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + 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: - Optional[TaskDict]: Task entity data or None if was not found. + str: Activity id. + + """ + con = get_server_api_connection() + return con.create_activity( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + activity_type=activity_type, + activity_id=activity_id, + body=body, + file_ids=file_ids, + timestamp=timestamp, + data=data, + ) + + +def update_activity( + project_name: str, + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[dict[str, Any]] = 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. + 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() - return con.get_task_by_id( + return con.update_activity( project_name=project_name, - task_id=task_id, - fields=fields, - own_attributes=own_attributes, + activity_id=activity_id, + body=body, + file_ids=file_ids, + append_file_ids=append_file_ids, + data=data, ) -def get_tasks_by_folder_paths( +def delete_activity( project_name: str, - folder_paths: Iterable[str], - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Dict[str, List["TaskDict"]]: - """Query task entities from server by folder paths. + activity_id: str, +): + """Delete activity by id. Args: - project_name (str): Name of project. - folder_paths (list[str]): Folder paths. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Dict[str, List[TaskDict]]: Task entities by - folder path. + project_name (str): Project on which activity happened. + activity_id (str): Activity id to remove. """ con = get_server_api_connection() - return con.get_tasks_by_folder_paths( + return con.delete_activity( project_name=project_name, - folder_paths=folder_paths, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + activity_id=activity_id, ) -def get_tasks_by_folder_path( +def send_activities_batch_operations( project_name: str, - folder_path: str, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> List["TaskDict"]: - """Query task entities from server by folder path. + operations: list, + can_fail: bool = False, + raise_on_fail: bool = True, +) -> list: + """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): Name of project. - folder_path (str): Folder path. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + 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.get_tasks_by_folder_path( + return con.send_activities_batch_operations( project_name=project_name, - folder_path=folder_path, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_task_by_folder_path( - project_name: str, - folder_path: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by folder path and task name. +def get_addon_endpoint( + addon_name: str, + addon_version: str, + *subpaths, +) -> str: + """Calculate endpoint to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' Args: - project_name (str): Project name. - folder_path (str): Folder path. - task_name (str): Task name. - fields (Optional[Iterable[str]]): Task fields that should - be returned. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + str: Final url. """ con = get_server_api_connection() - return con.get_task_by_folder_path( - project_name=project_name, - folder_path=folder_path, - task_name=task_name, - fields=fields, - own_attributes=own_attributes, + return con.get_addon_endpoint( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, ) -def create_task( - project_name: str, - name: str, - task_type: str, - folder_id: str, - label: Optional[str] = None, - assignees: Optional[Iterable[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - task_id: Optional[str] = None, -) -> str: - """Create new task. +def get_addons_info( + details: bool = True, +) -> "AddonsInfoDict": + """Get information about addons available on server. Args: - project_name (str): Project name. - name (str): Folder name. - task_type (str): Task type. - folder_id (str): Parent folder id. - label (Optional[str]): Label of folder. - assignees (Optional[Iterable[str]]): Task assignees. - attrib (Optional[dict[str, Any]]): Task attributes. - data (Optional[dict[str, Any]]): Task data. - tags (Optional[Iterable[str]]): Task tags. - status (Optional[str]): Task status. - active (Optional[bool]): Task active state. - thumbnail_id (Optional[str]): Task thumbnail id. - task_id (Optional[str]): Task id. If not passed new id is - generated. - - Returns: - str: Task id. + details (Optional[bool]): Detailed data with information how + to get client code. """ con = get_server_api_connection() - return con.create_task( - project_name=project_name, - name=name, - task_type=task_type, - folder_id=folder_id, - label=label, - assignees=assignees, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - task_id=task_id, + return con.get_addons_info( + details=details, ) -def update_task( - project_name: str, - task_id: str, - name: Optional[str] = None, - task_type: Optional[str] = None, - folder_id: Optional[str] = None, - label: Optional[str] = NOT_SET, - assignees: Optional[List[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, -): - """Update task entity on server. - - Do not pass ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. +def get_addon_url( + addon_name: str, + addon_version: str, + *subpaths, + use_rest: bool = True, +) -> str: + """Calculate url to addon route. - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + Examples: + + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' Args: - project_name (str): Project name. - task_id (str): Task id. - name (Optional[str]): New name. - task_type (Optional[str]): New task type. - folder_id (Optional[str]): New folder id. - label (Optional[Union[str, None]]): New label. - assignees (Optional[str]): New assignees. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. + + Returns: + str: Final url. """ con = get_server_api_connection() - return con.update_task( - project_name=project_name, - task_id=task_id, - name=name, - task_type=task_type, - folder_id=folder_id, - label=label, - assignees=assignees, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, + return con.get_addon_url( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + use_rest=use_rest, ) -def delete_task( - project_name: str, - task_id: str, -): - """Delete task. +def delete_addon( + addon_name: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon from server. + + Delete all versions of addon from server. Args: - project_name (str): Project name. - task_id (str): Task id to delete. + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.delete_task( - project_name=project_name, - task_id=task_id, + return con.delete_addon( + addon_name=addon_name, + purge=purge, ) -def get_products( - project_name: str, - product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - product_types: Optional[Iterable[str]] = None, - product_name_regex: Optional[str] = None, - product_path_regex: Optional[str] = None, - names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["ProductDict", None, None]: - """Query products from server. +def delete_addon_version( + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon version from server. - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. + Delete all versions of addon from server. Args: - project_name (str): Name of project. - product_ids (Optional[Iterable[str]]): Task ids to filter. - product_names (Optional[Iterable[str]]): Task names used for - filtering. - folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. - product_types (Optional[Iterable[str]]): Product types used for - filtering. - product_name_regex (Optional[str]): Filter products by name regex. - product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. - names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product - name filtering by folder id. - statuses (Optional[Iterable[str]]): Product statuses used - for filtering. - tags (Optional[Iterable[str]]): Product tags used - for filtering. - active (Optional[bool]): Filter active/inactive products. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Generator[ProductDict, None, None]: Queried product entities. + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_products( - project_name=project_name, - product_ids=product_ids, - product_names=product_names, - folder_ids=folder_ids, - product_types=product_types, - product_name_regex=product_name_regex, - product_path_regex=product_path_regex, - names_by_folder_ids=names_by_folder_ids, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + return con.delete_addon_version( + addon_name=addon_name, + addon_version=addon_version, + purge=purge, ) -def get_product_by_id( - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by id. +def upload_addon_zip( + src_filepath: str, + progress: Optional[TransferProgress] = None, +): + """Upload addon zip file to server. + + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). + + Example output:: + + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} Args: - project_name (str): Name of project where to look for queried - entities. - product_id (str): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. Returns: - Optional[ProductDict]: Product entity data or None - if was not found. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.get_product_by_id( - project_name=project_name, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, + return con.upload_addon_zip( + src_filepath=src_filepath, + progress=progress, ) -def get_product_by_name( - project_name: str, - product_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by name and folder id. +def download_addon_private_file( + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download a file from addon private files. + + This method requires to have authorized token available. Private files + are not under '/api' restpoint. Args: - project_name (str): Name of project where to look for queried - entities. - product_name (str): Product name. - folder_id (str): Folder id (Folder is a parent of products). - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. Returns: - Optional[ProductDict]: Product entity data or None - if was not found. + str: Filepath to downloaded file. """ con = get_server_api_connection() - return con.get_product_by_name( - project_name=project_name, - product_name=product_name, - folder_id=folder_id, - fields=fields, - own_attributes=own_attributes, + return con.download_addon_private_file( + addon_name=addon_name, + addon_version=addon_version, + filename=filename, + destination_dir=destination_dir, + destination_filename=destination_filename, + chunk_size=chunk_size, + progress=progress, ) -def get_product_types( - fields: Optional[Iterable[str]] = None, -) -> List["ProductTypeDict"]: - """Types of products. +def get_event( + event_id: str, +) -> Optional[dict[str, Any]]: + """Query full event data by id. - This is server wide information. Product types have 'name', 'icon' and - 'color'. + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. Args: - fields (Optional[Iterable[str]]): Product types fields to query. + event_id (str): Event id. Returns: - list[ProductTypeDict]: Product types information. + dict[str, Any]: Full event data. """ con = get_server_api_connection() - return con.get_product_types( - fields=fields, + return con.get_event( + event_id=event_id, ) -def get_project_product_types( - project_name: str, +def get_events( + topics: Optional[Iterable[str]] = None, + event_ids: Optional[Iterable[str]] = None, + project_names: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + users: Optional[Iterable[str]] = None, + include_logs: Optional[bool] = None, + has_children: Optional[bool] = None, + newer_than: Optional[str] = None, + older_than: Optional[str] = None, fields: Optional[Iterable[str]] = None, -) -> List["ProductTypeDict"]: - """DEPRECATED Types of products available in a project. + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + states: Optional[Iterable[str]] = None, +) -> Generator[dict[str, Any], None, None]: + """Get events from server with filtering options. - Filter only product types available in a project. + Notes: + Not all event happen on a project. Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. + topics (Optional[Iterable[str]]): Name of topics. + event_ids (Optional[Iterable[str]]): Event ids. + project_names (Optional[Iterable[str]]): Project on which + event happened. + statuses (Optional[Iterable[str]]): Filtering by statuses. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + has_children (Optional[bool]): Event is with/without children + events. If 'None' then all events are returned, default. + newer_than (Optional[str]): Return only events newer than given + iso datetime string. + older_than (Optional[str]): Return only events older than given + iso datetime string. + fields (Optional[Iterable[str]]): Fields that should be received + for each event. + limit (Optional[int]): Limit number of events to be fetched. + order (Optional[SortOrder]): Order events in ascending + or descending order. It is recommended to set 'limit' + when used descending. + states (Optional[Iterable[str]]): DEPRECATED Filtering by states. + Use 'statuses' instead. Returns: - List[ProductTypeDict]: Product types information. + Generator[dict[str, Any]]: Available events matching filters. """ con = get_server_api_connection() - return con.get_project_product_types( - project_name=project_name, + return con.get_events( + topics=topics, + event_ids=event_ids, + project_names=project_names, + statuses=statuses, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, fields=fields, + limit=limit, + order=order, + states=states, ) -def get_product_type_names( +def update_event( + event_id: str, + sender: Optional[str] = None, project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, -) -> Set[str]: - """DEPRECATED Product type names. - - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. + username: Optional[str] = None, + status: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + progress: Optional[int] = None, + retries: Optional[int] = None, +): + """Update event data. Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. - - Returns: - set[str]: Product type names. + event_id (str): Event id. + sender (Optional[str]): New sender of event. + project_name (Optional[str]): New project name. + username (Optional[str]): New username. + status (Optional[str]): New event status. Enum: "pending", + "in_progress", "finished", "failed", "aborted", "restarted" + description (Optional[str]): New description. + summary (Optional[dict[str, Any]]): New summary. + payload (Optional[dict[str, Any]]): New payload. + progress (Optional[int]): New progress. Range [0-100]. + retries (Optional[int]): New retries. """ con = get_server_api_connection() - return con.get_product_type_names( + return con.update_event( + event_id=event_id, + sender=sender, project_name=project_name, - product_ids=product_ids, + username=username, + status=status, + description=description, + summary=summary, + payload=payload, + progress=progress, + retries=retries, ) -def create_product( - project_name: str, - name: str, - product_type: str, - folder_id: str, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: "Union[bool, None]" = None, - product_id: Optional[str] = None, -) -> str: - """Create new product. +def dispatch_event( + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, +): + """Dispatch event to server. Args: - project_name (str): Project name. - name (str): Product name. - product_type (str): Product type. - folder_id (str): Parent folder id. - attrib (Optional[dict[str, Any]]): Product attributes. - data (Optional[dict[str, Any]]): Product data. - tags (Optional[Iterable[str]]): Product tags. - status (Optional[str]): Product status. - active (Optional[bool]): Product active state. - product_id (Optional[str]): Product id. If not passed new id is - generated. + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (Optional[bool]): Mark event as finished on dispatch. + store (Optional[bool]): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. Returns: - str: Product id. + RestApiResponse: Response from server. """ con = get_server_api_connection() - return con.create_product( + return con.dispatch_event( + topic=topic, + sender=sender, + event_hash=event_hash, project_name=project_name, - name=name, - product_type=product_type, - folder_id=folder_id, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - product_id=product_id, + username=username, + depends_on=depends_on, + description=description, + summary=summary, + payload=payload, + finished=finished, + store=store, + dependencies=dependencies, ) -def update_product( - project_name: str, - product_id: str, - name: Optional[str] = None, - folder_id: Optional[str] = None, - product_type: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, +def delete_event( + event_id: str, ): - """Update product entity on server. - - Update of ``data`` will override existing value on folder entity. + """Delete event by id. - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + Supported since AYON server 1.6.0. Args: - project_name (str): Project name. - product_id (str): Product id. - name (Optional[str]): New product name. - folder_id (Optional[str]): New product id. - product_type (Optional[str]): New product type. - attrib (Optional[dict[str, Any]]): New product attributes. - data (Optional[dict[str, Any]]): New product data. - tags (Optional[Iterable[str]]): New product tags. - status (Optional[str]): New product status. - active (Optional[bool]): New product active state. + event_id (str): Event id. + + Returns: + RestApiResponse: Response from server. """ con = get_server_api_connection() - return con.update_product( - project_name=project_name, - product_id=product_id, - name=name, - folder_id=folder_id, - product_type=product_type, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, + return con.delete_event( + event_id=event_id, ) -def delete_product( - project_name: str, - product_id: str, -): - """Delete product. +def enroll_event_job( + source_topic: "Union[str, list[str]]", + target_topic: str, + sender: str, + description: Optional[str] = None, + sequential: Optional[bool] = None, + events_filter: Optional["EventFilter"] = None, + max_retries: Optional[int] = None, + ignore_older_than: Optional[str] = None, + ignore_sender_types: Optional[str] = None, +): + """Enroll job based on events. + + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. + + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. - Args: - project_name (str): Project name. - product_id (str): Product id to delete. + Make sure the new event has updated status to '"finished"' status + when you're done with logic - """ - con = get_server_api_connection() - return con.delete_product( - project_name=project_name, - product_id=product_id, - ) + Target topic should not clash with other processes/services. + Created target event have 'dependsOn' key where is id of source topic. -def get_versions( - project_name: str, - version_ids: Optional[Iterable[str]] = None, - product_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - versions: Optional[Iterable[str]] = None, - hero: bool = True, - standard: bool = True, - latest: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Get version entities based on passed filters from server. + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1-n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1-n machines + - order of events is not important + - 'sequential' should be 'False' Args: - project_name (str): Name of project where to look for versions. - version_ids (Optional[Iterable[str]]): Version ids used for - version filtering. - product_ids (Optional[Iterable[str]]): Product ids used for - version filtering. - task_ids (Optional[Iterable[str]]): Task ids used for - version filtering. - versions (Optional[Iterable[int]]): Versions we're interested in. - hero (Optional[bool]): Skip hero versions when set to False. - standard (Optional[bool]): Skip standard (non-hero) when - set to False. - latest (Optional[bool]): Return only latest version of standard - versions. This can be combined only with 'standard' attribute - set to True. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields to be queried - for version. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + source_topic (Union[str, list[str]]): Source topic to enroll with + wildcards '*', or explicit list of topics. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). + ignore_older_than (Optional[int]): Ignore events older than + given number in days. + ignore_sender_types (Optional[list[str]]): Ignore events triggered + by given sender types. Returns: - Generator[VersionDict, None, None]: Queried version entities. + Optional[dict[str, Any]]: None if there is no event matching + filters. Created event with 'target_topic'. """ con = get_server_api_connection() - return con.get_versions( - project_name=project_name, - version_ids=version_ids, - product_ids=product_ids, - task_ids=task_ids, - versions=versions, - hero=hero, - standard=standard, - latest=latest, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + return con.enroll_event_job( + source_topic=source_topic, + target_topic=target_topic, + sender=sender, + description=description, + sequential=sequential, + events_filter=events_filter, + max_retries=max_retries, + ignore_older_than=ignore_older_than, + ignore_sender_types=ignore_sender_types, ) -def get_version_by_id( - project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by id. +def get_full_link_type_name( + link_type_name: str, + input_type: str, + output_type: str, +) -> str: + """Calculate full link type name used for query from server. Args: - project_name (str): Name of project where to look for queried - entities. - version_id (str): Version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + link_type_name (str): Type of link. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + str: Full name of link type used for query from server. """ con = get_server_api_connection() - return con.get_version_by_id( - project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + return con.get_full_link_type_name( + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_version_by_name( +def get_link_types( project_name: str, - version: int, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by version and product id. +) -> list[dict[str, Any]]: + """All link types available on a project. + + Example output: + [ + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + ] Args: - project_name (str): Name of project where to look for queried - entities. - version (int): Version of version entity. - product_id (str): Product id. Product is a parent of version. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Name of project where to look for link types. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + list[dict[str, Any]]: Link types available on project. """ con = get_server_api_connection() - return con.get_version_by_name( + return con.get_link_types( project_name=project_name, - version=version, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, ) -def get_hero_version_by_id( +def get_link_type( project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by id. + link_type_name: str, + input_type: str, + output_type: str, +) -> Optional[dict[str, Any]]: + """Get link type data. + + There is not dedicated REST endpoint to get single link type, + so method 'get_link_types' is used. + + Example output: + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } Args: - project_name (str): Name of project where to look for queried - entities. - version_id (int): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where link type is available. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Optional[dict[str, Any]]: Link type information. """ con = get_server_api_connection() - return con.get_hero_version_by_id( + return con.get_link_type( project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) - -def get_hero_version_by_product_id( - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by product id. - - Only one hero version is available on a product. + +def create_link_type( + project_name: str, + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, +): + """Create or update link type on server. + + Warning: + Because PUT is used for creation it is also used for update. Args: - project_name (str): Name of project where to look for queried - entities. - product_id (int): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Additional data related to link. - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_hero_version_by_product_id( + return con.create_link_type( project_name=project_name, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def get_hero_versions( +def delete_link_type( project_name: str, - product_ids: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Query hero versions by multiple filters. - - Only one hero version is available on a product. + link_type_name: str, + input_type: str, + output_type: str, +): + """Remove link type from project. Args: - project_name (str): Name of project where to look for queried - entities. - product_ids (Optional[Iterable[str]]): Product ids. - version_ids (Optional[Iterable[str]]): Version ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_hero_versions( + return con.delete_link_type( project_name=project_name, - product_ids=product_ids, - version_ids=version_ids, - active=active, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_last_versions( +def make_sure_link_type_exists( project_name: str, - product_ids: Iterable[str], - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Dict[str, Optional["VersionDict"]]: - """Query last version entities by product ids. + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, +): + """Make sure link type exists on a project. Args: - project_name (str): Project where to look for representation. - product_ids (Iterable[str]): Product ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - dict[str, Optional[VersionDict]]: Last versions by product id. + project_name (str): Name of project. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Link type related data. """ con = get_server_api_connection() - return con.get_last_versions( + return con.make_sure_link_type_exists( project_name=project_name, - product_ids=product_ids, - active=active, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def get_last_version_by_product_id( +def create_link( project_name: str, - product_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product id. + link_type_name: str, + input_id: str, + input_type: str, + output_id: str, + output_type: str, + link_name: Optional[str] = None, +): + """Create link between 2 entities. + + Link has a type which must already exists on a project. + + Example output:: + + { + "id": "59a212c0d2e211eda0e20242ac120002" + } Args: - project_name (str): Project where to look for representation. - product_id (str): Product id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where the link is created. + link_type_name (str): Type of link. + input_id (str): Input entity id. + input_type (str): Entity type of input entity. + output_id (str): Output entity id. + output_type (str): Entity type of output entity. + link_name (Optional[str]): Name of link. + Available from server version '1.0.0-rc.6'. Returns: - Optional[VersionDict]: Queried version entity or None. + dict[str, str]: Information about link. + + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_last_version_by_product_id( + return con.create_link( project_name=project_name, - product_id=product_id, - active=active, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_id=input_id, + input_type=input_type, + output_id=output_id, + output_type=output_type, + link_name=link_name, ) -def get_last_version_by_product_name( +def delete_link( project_name: str, - product_name: str, - folder_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product name and folder id. + link_id: str, +): + """Remove link by id. Args: - project_name (str): Project where to look for representation. - product_name (str): Product name. - folder_id (str): Folder id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (str): Project where link exists. + link_id (str): Id of link. - Returns: - Optional[VersionDict]: Queried version entity or None. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_last_version_by_product_name( + return con.delete_link( project_name=project_name, - product_name=product_name, - folder_id=folder_id, - active=active, - fields=fields, - own_attributes=own_attributes, + link_id=link_id, ) -def version_is_latest( +def get_entities_links( project_name: str, - version_id: str, -) -> bool: - """Is version latest from a product. + entity_type: str, + entity_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + link_names: Optional[Iterable[str]] = None, + link_name_regex: Optional[str] = None, +) -> dict[str, list[dict[str, Any]]]: + """Helper method to get links from server for entity types. + + .. highlight:: text + .. code-block:: text + + Example output: + { + "59a212c0d2e211eda0e20242ac120001": [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ], + ... + } Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. + project_name (str): Project where links are. + entity_type (Literal["folder", "task", "product", + "version", "representations"]): Entity type. + entity_ids (Optional[Iterable[str]]): Ids of entities for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. Returns: - bool: Version is latest or not. + dict[str, list[dict[str, Any]]]: Link info by entity ids. """ con = get_server_api_connection() - return con.version_is_latest( + return con.get_entities_links( project_name=project_name, - version_id=version_id, + entity_type=entity_type, + entity_ids=entity_ids, + link_types=link_types, + link_direction=link_direction, + link_names=link_names, + link_name_regex=link_name_regex, ) -def create_version( +def get_folders_links( project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, -) -> str: - """Create new version. + folder_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query folders links from server. Args: - project_name (str): Project name. - version (int): Version. - product_id (str): Parent product id. - task_id (Optional[str]): Parent task id. - author (Optional[str]): Version author. - attrib (Optional[dict[str, Any]]): Version attributes. - data (Optional[dict[str, Any]]): Version data. - tags (Optional[Iterable[str]]): Version tags. - status (Optional[str]): Version status. - active (Optional[bool]): Version active state. - thumbnail_id (Optional[str]): Version thumbnail id. - version_id (Optional[str]): Version id. If not passed new id is - generated. + project_name (str): Project where links are. + folder_ids (Optional[Iterable[str]]): Ids of folders for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - str: Version id. + dict[str, list[dict[str, Any]]]: Link info by folder ids. """ con = get_server_api_connection() - return con.create_version( + return con.get_folders_links( project_name=project_name, - version=version, - product_id=product_id, - task_id=task_id, - author=author, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - version_id=version_id, + folder_ids=folder_ids, + link_types=link_types, + link_direction=link_direction, ) -def update_version( +def get_folder_links( project_name: str, - version_id: str, - version: Optional[int] = None, - product_id: Optional[str] = None, - task_id: Optional[str] = NOT_SET, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, -): - """Update version entity on server. - - Do not pass ``task_id`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + folder_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query folder links from server. Args: - project_name (str): Project name. - version_id (str): Version id. - version (Optional[int]): New version. - product_id (Optional[str]): New product id. - task_id (Optional[Union[str, None]]): New task id. - author (Optional[str]): New author username. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - con = get_server_api_connection() - return con.update_version( - project_name=project_name, - version_id=version_id, - version=version, - product_id=product_id, - task_id=task_id, - author=author, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - ) - - -def delete_version( - project_name: str, - version_id: str, -): - """Delete version. + project_name (str): Project where links are. + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. - Args: - project_name (str): Project name. - version_id (str): Version id to delete. + Returns: + list[dict[str, Any]]: Link info of folder. """ con = get_server_api_connection() - return con.delete_version( + return con.get_folder_links( project_name=project_name, - version_id=version_id, + folder_id=folder_id, + link_types=link_types, + link_direction=link_direction, ) -def send_batch_operations( +def get_tasks_links( project_name: str, - operations: List[Dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: - """Post multiple CRUD 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. + task_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query tasks links from server. 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. + project_name (str): Project where links are. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - list[dict[str, Any]]: Operations result with process details. + dict[str, list[dict[str, Any]]]: Link info by task ids. """ con = get_server_api_connection() - return con.send_batch_operations( + return con.get_tasks_links( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + task_ids=task_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. +def get_task_links( + project_name: str, + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query task links from server. Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - List[ActionManifestDict]: List of action manifests. + list[dict[str, Any]]: Link info of task. """ con = get_server_api_connection() - return con.get_actions( + return con.get_task_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, - mode=mode, - ) - - -def trigger_action( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. + task_id=task_id, + link_types=link_types, + link_direction=link_direction, + ) + + +def get_products_links( + project_name: str, + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by product ids. """ con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.get_products_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + product_ids=product_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_action_config( - identifier: str, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. +def get_product_links( + project_name: str, + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query product links from server. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionConfigResponse: Action configuration data. + list[dict[str, Any]]: Link info of product. """ con = get_server_api_connection() - return con.get_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, + return con.get_product_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + product_id=product_id, + link_types=link_types, + link_direction=link_direction, ) -def set_action_config( - identifier: str, - addon_name: str, - addon_version: str, - value: Dict[str, Any], - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Set action configuration. +def get_versions_links( + project_name: str, + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action - configuration. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionConfigResponse: New action configuration data. + dict[str, list[dict[str, Any]]]: Link info by version ids. """ con = get_server_api_connection() - return con.set_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - value=value, + return con.get_versions_links( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + version_ids=version_ids, + link_types=link_types, + link_direction=link_direction, ) -def take_action( - action_token: str, -) -> "ActionTakeResponse": - """Take action metadata using an action token. +def get_version_links( + project_name: str, + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query version links from server. Args: - action_token (str): AYON launcher action token. + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ActionTakeResponse: Action metadata describing how to launch - action. + list[dict[str, Any]]: Link info of version. """ con = get_server_api_connection() - return con.take_action( - action_token=action_token, + return con.get_version_links( + project_name=project_name, + version_id=version_id, + link_types=link_types, + link_direction=link_direction, ) -def abort_action( - action_token: str, - message: Optional[str] = None, -) -> None: - """Abort action using an action token. +def get_representations_links( + project_name: str, + representation_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query representations links from server. Args: - action_token (str): AYON launcher action token. - message (Optional[str]): Message to display in the UI. + project_name (str): Project where links are. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by representation ids. """ con = get_server_api_connection() - return con.abort_action( - action_token=action_token, - message=message, + return con.get_representations_links( + project_name=project_name, + representation_ids=representation_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_activities( +def get_representation_links( project_name: str, - 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["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, -) -> Generator[dict[str, Any], None, None]: - """Get activities from server with filtering options. + representation_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query representation links from server. 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[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. + project_name (str): Project where links are. + representation_id (str): Representation id for which links + should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - Generator[dict[str, Any]]: Available activities matching filters. + list[dict[str, Any]]: Link info of representation. """ con = get_server_api_connection() - return con.get_activities( + return con.get_representation_links( project_name=project_name, - activity_ids=activity_ids, - activity_types=activity_types, - entity_ids=entity_ids, - entity_names=entity_names, - entity_type=entity_type, - changed_after=changed_after, - changed_before=changed_before, - reference_types=reference_types, - fields=fields, - limit=limit, - order=order, + representation_id=representation_id, + link_types=link_types, + link_direction=link_direction, ) -def get_activity_by_id( +def get_entity_lists( project_name: str, - activity_id: str, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, -) -> Optional[dict[str, Any]]: - """Get activity by id. +) -> Generator[Dict[str, Any], None, None]: + """Fetch entity lists from server. Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Optional[dict[str, Any]]: Activity data or None if activity is not - found. + Generator[Dict[str, Any], None, None]: Entity list entities + matching defined filters. """ con = get_server_api_connection() - return con.get_activity_by_id( + return con.get_entity_lists( project_name=project_name, - activity_id=activity_id, - reference_types=reference_types, + list_ids=list_ids, + active=active, fields=fields, ) -def create_activity( +def get_entity_list_rest( 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, -) -> str: - """Create activity on a project. + list_id: str, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using REST API. 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. + project_name (str): Project name. + list_id (str): Entity list id. Returns: - str: Activity id. + Optional[Dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.create_activity( + return con.get_entity_list_rest( project_name=project_name, - entity_id=entity_id, - entity_type=entity_type, - activity_type=activity_type, - activity_id=activity_id, - body=body, - file_ids=file_ids, - timestamp=timestamp, - data=data, + list_id=list_id, ) -def update_activity( +def get_entity_list_by_id( project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[list[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[dict[str, Any]] = None, -): - """Update activity by id. + list_id: str, + fields: Optional[Iterable[str]] = None, +) -> Optional[Dict[str, Any]]: + """Get entity list by id using GraphQl. 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. - append_file_ids (Optional[bool]): Append file ids to existing - list of file ids. - data (Optional[dict[str, Any]]): Update data in activity. + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. + + Returns: + Optional[Dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.update_activity( + return con.get_entity_list_by_id( project_name=project_name, - activity_id=activity_id, - body=body, - file_ids=file_ids, - append_file_ids=append_file_ids, - data=data, + list_id=list_id, + fields=fields, ) -def delete_activity( +def create_entity_list( project_name: str, - activity_id: str, -): - """Delete activity by id. + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + template: Optional[Dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[List[Dict[str, Any]]] = None, + list_id: Optional[str] = None, +) -> str: + """Create entity list. Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id to remove. + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. """ con = get_server_api_connection() - return con.delete_activity( + return con.create_entity_list( project_name=project_name, - activity_id=activity_id, + entity_type=entity_type, + label=label, + list_type=list_type, + access=access, + attrib=attrib, + data=data, + tags=tags, + template=template, + owner=owner, + active=active, + items=items, + list_id=list_id, ) -def send_activities_batch_operations( +def update_entity_list( project_name: str, - operations: list, - can_fail: bool = False, - raise_on_fail: bool = True, -) -> list: - """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. + list_id: str, + *, + label: Optional[str] = None, + access: Optional[Dict[str, Any]] = None, + attrib: Optional[List[Dict[str, Any]]] = None, + data: Optional[List[Dict[str, Any]]] = None, + tags: Optional[List[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, +) -> None: + """Update entity list. 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'. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. - 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. + """ + con = get_server_api_connection() + return con.update_entity_list( + project_name=project_name, + list_id=list_id, + label=label, + access=access, + attrib=attrib, + data=data, + tags=tags, + owner=owner, + active=active, + ) - Returns: - list[dict[str, Any]]: Operations result with process details. + +def delete_entity_list( + project_name: str, + list_id: str, +) -> None: + """Delete entity list from project. + + Args: + project_name (str): Project name. + list_id (str): Entity list id that will be removed. """ con = get_server_api_connection() - return con.send_activities_batch_operations( + return con.delete_entity_list( project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + list_id=list_id, ) -def get_addon_endpoint( - addon_name: str, - addon_version: str, - *subpaths, -) -> str: - """Calculate endpoint to addon route. - - Examples: - >>> from ayon_api import ServerAPI - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' +def get_entity_list_attribute_definitions( + project_name: str, + list_id: str, +) -> List["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. + project_name (str): Project name. + list_id (str): Entity list id. Returns: - str: Final url. + List[EntityListAttributeDefinitionDict]: List of attribute + definitions. """ con = get_server_api_connection() - return con.get_addon_endpoint( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, + return con.get_entity_list_attribute_definitions( + project_name=project_name, + list_id=list_id, ) -def get_addons_info( - details: bool = True, -) -> "AddonsInfoDict": - """Get information about addons available on server. +def set_entity_list_attribute_definitions( + project_name: str, + list_id: str, + attribute_definitions: List["EntityListAttributeDefinitionDict"], +) -> None: + """Set attribute definitioins on entity list. Args: - details (Optional[bool]): Detailed data with information how - to get client code. + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (List[EntityListAttributeDefinitionDict]): + List of attribute definitions. """ con = get_server_api_connection() - return con.get_addons_info( - details=details, + return con.set_entity_list_attribute_definitions( + project_name=project_name, + list_id=list_id, + attribute_definitions=attribute_definitions, ) -def get_addon_url( - addon_name: str, - addon_version: str, - *subpaths, - use_rest: bool = True, +def create_entity_list_item( + project_name: str, + list_id: str, + *, + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, + item_id: Optional[str] = None, ) -> str: - """Calculate url to addon route. - - Examples: - - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + """Create entity list item. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. Returns: - str: Final url. + str: Item id. """ con = get_server_api_connection() - return con.get_addon_url( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - use_rest=use_rest, + return con.create_entity_list_item( + project_name=project_name, + list_id=list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + item_id=item_id, ) -def delete_addon( - addon_name: str, - purge: Optional[bool] = None, +def update_entity_list_items( + project_name: str, + list_id: str, + items: List[Dict[str, Any]], + mode: "EntityListItemMode", ) -> None: - """Delete addon from server. - - Delete all versions of addon from server. + """Update items in entity list. Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (List[Dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. """ con = get_server_api_connection() - return con.delete_addon( - addon_name=addon_name, - purge=purge, + return con.update_entity_list_items( + project_name=project_name, + list_id=list_id, + items=items, + mode=mode, ) -def delete_addon_version( - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, +def update_entity_list_item( + project_name: str, + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[Dict[str, Any]] = None, + data: Optional[Dict[str, Any]] = None, + tags: Optional[List[str]] = None, ) -> None: - """Delete addon version from server. - - Delete all versions of addon from server. + """Update item in entity list. Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. """ con = get_server_api_connection() - return con.delete_addon_version( - addon_name=addon_name, - addon_version=addon_version, - purge=purge, + return con.update_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, + new_list_id=new_list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, ) -def upload_addon_zip( - src_filepath: str, - progress: Optional[TransferProgress] = None, -): - """Upload addon zip file to server. +def delete_entity_list_item( + project_name: str, + list_id: str, + item_id: str, +) -> None: + """Delete item from entity list. - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. - Example output:: + """ + con = get_server_api_connection() + return con.delete_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, + ) - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + +def get_rest_project( + project_name: str, +) -> Optional["ProjectDict"]: + """Query project by name. + + This call returns project with anatomy data. Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. + project_name (str): Name of project. Returns: - dict[str, Any]: Response data from server. + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. """ con = get_server_api_connection() - return con.upload_addon_zip( - src_filepath=src_filepath, - progress=progress, + return con.get_rest_project( + project_name=project_name, ) -def download_addon_private_file( - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download a file from addon private files. - - This method requires to have authorized token available. Private files - are not under '/api' restpoint. - - Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. +def get_rest_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> Generator["ProjectDict", None, None]: + """Query available project entities. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. Returns: - str: Filepath to downloaded file. + Generator[ProjectDict, None, None]: Available projects. """ con = get_server_api_connection() - return con.download_addon_private_file( - addon_name=addon_name, - addon_version=addon_version, - filename=filename, - destination_dir=destination_dir, - destination_filename=destination_filename, - chunk_size=chunk_size, - progress=progress, + return con.get_rest_projects( + active=active, + library=library, ) -def get_event( - event_id: str, -) -> Optional[dict[str, Any]]: - """Query full event data by id. +def get_project_names( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> list[str]: + """Receive available project names. - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. + User must be logged in. Args: - event_id (str): Event id. + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. Returns: - dict[str, Any]: Full event data. + list[str]: List of available project names. """ con = get_server_api_connection() - return con.get_event( - event_id=event_id, + return con.get_project_names( + active=active, + library=library, ) -def get_events( - topics: Optional[Iterable[str]] = None, - event_ids: Optional[Iterable[str]] = None, - project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - users: Optional[Iterable[str]] = None, - include_logs: Optional[bool] = None, - has_children: Optional[bool] = None, - newer_than: Optional[str] = None, - older_than: Optional[str] = None, +def get_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - states: Optional[Iterable[str]] = None, -) -> Generator[dict[str, Any], None, None]: - """Get events from server with filtering options. - - Notes: - Not all event happen on a project. + own_attributes: bool = False, +) -> Generator["ProjectDict", None, None]: + """Get projects. Args: - topics (Optional[Iterable[str]]): Name of topics. - event_ids (Optional[Iterable[str]]): Event ids. - project_names (Optional[Iterable[str]]): Project on which - event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. - users (Optional[Iterable[str]]): Filtering by users - who created/triggered an event. - include_logs (Optional[bool]): Query also log events. - has_children (Optional[bool]): Event is with/without children - events. If 'None' then all events are returned, default. - newer_than (Optional[str]): Return only events newer than given - iso datetime string. - older_than (Optional[str]): Return only events older than given - iso datetime string. - fields (Optional[Iterable[str]]): Fields that should be received - for each event. - limit (Optional[int]): Limit number of events to be fetched. - order (Optional[SortOrder]): Order events in ascending - or descending order. It is recommended to set 'limit' - when used descending. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. + active (Optional[bool]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Optional[bool]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Generator[dict[str, Any]]: Available events matching filters. + Generator[ProjectDict, None, None]: Queried projects. """ con = get_server_api_connection() - return con.get_events( - topics=topics, - event_ids=event_ids, - project_names=project_names, - statuses=statuses, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, + return con.get_projects( + active=active, + library=library, fields=fields, - limit=limit, - order=order, - states=states, + own_attributes=own_attributes, ) -def update_event( - event_id: str, - sender: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - status: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[dict[str, Any]] = None, - payload: Optional[dict[str, Any]] = None, - progress: Optional[int] = None, - retries: Optional[int] = None, -): - """Update event data. +def get_project( + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["ProjectDict"]: + """Get project. Args: - event_id (str): Event id. - sender (Optional[str]): New sender of event. - project_name (Optional[str]): New project name. - username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", - "in_progress", "finished", "failed", "aborted", "restarted" - description (Optional[str]): New description. - summary (Optional[dict[str, Any]]): New summary. - payload (Optional[dict[str, Any]]): New payload. - progress (Optional[int]): New progress. Range [0-100]. - retries (Optional[int]): New retries. + project_name (str): Name of project. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[ProjectDict]: Project entity data or None + if project was not found. """ con = get_server_api_connection() - return con.update_event( - event_id=event_id, - sender=sender, + return con.get_project( project_name=project_name, - username=username, - status=status, - description=description, - summary=summary, - payload=payload, - progress=progress, - retries=retries, + fields=fields, + own_attributes=own_attributes, ) -def dispatch_event( - topic: str, - sender: Optional[str] = None, - event_hash: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - depends_on: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[dict[str, Any]] = None, - payload: Optional[dict[str, Any]] = None, - finished: bool = True, - store: bool = True, - dependencies: Optional[list[str]] = None, -): - """Dispatch event to server. +def create_project( + project_name: str, + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, +) -> "ProjectDict": + """Create project using AYON settings. + + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. Args: - topic (str): Event topic used for filtering of listeners. - sender (Optional[str]): Sender of event. - event_hash (Optional[str]): Event hash. - project_name (Optional[str]): Project name. - depends_on (Optional[str]): Add dependency to another event. - username (Optional[str]): Username which triggered event. - description (Optional[str]): Description of event. - summary (Optional[dict[str, Any]]): Summary of event that can - be used for simple filtering on listeners. - payload (Optional[dict[str, Any]]): Full payload of event data with - all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible - future processing otherwise is event send only - to active listeners. - dependencies (Optional[list[str]]): Deprecated. - List of event id dependencies. + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. Returns: - RestApiResponse: Response from server. + ProjectDict: Created project entity. """ con = get_server_api_connection() - return con.dispatch_event( - topic=topic, - sender=sender, - event_hash=event_hash, + return con.create_project( project_name=project_name, - username=username, - depends_on=depends_on, - description=description, - summary=summary, - payload=payload, - finished=finished, - store=store, - dependencies=dependencies, + project_code=project_code, + library_project=library_project, + preset_name=preset_name, ) -def delete_event( - event_id: str, +def update_project( + project_name: str, + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes, ): - """Delete event by id. - - Supported since AYON server 1.6.0. + """Update project entity on server. Args: - event_id (str): Event id. - - Returns: - RestApiResponse: Response from server. + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. """ con = get_server_api_connection() - return con.delete_event( - event_id=event_id, + return con.update_project( + project_name=project_name, + library=library, + folder_types=folder_types, + task_types=task_types, + link_types=link_types, + statuses=statuses, + tags=tags, + config=config, + attrib=attrib, + data=data, + active=active, + project_code=project_code, + **changes, ) -def enroll_event_job( - source_topic: "Union[str, list[str]]", - target_topic: str, - sender: str, - description: Optional[str] = None, - sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, - max_retries: Optional[int] = None, - ignore_older_than: Optional[str] = None, - ignore_sender_types: Optional[str] = None, +def delete_project( + project_name: str, ): - """Enroll job based on events. - - Enroll will find first unprocessed event with 'source_topic' and will - create new event with 'target_topic' for it and return the new event - data. - - Use 'sequential' to control that only single target event is created - at same time. Creation of new target events is blocked while there is - at least one unfinished event with target topic, when set to 'True'. - This helps when order of events matter and more than one process using - the same target is running at the same time. - - Make sure the new event has updated status to '"finished"' status - when you're done with logic - - Target topic should not clash with other processes/services. - - Created target event have 'dependsOn' key where is id of source topic. + """Delete project from server. - Use-case: - - Service 1 is creating events with topic 'my.leech' - - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1-n machines - - all events must be processed in a sequence by their creation - time and only one event can be processed at a time - - in this case 'sequential' should be set to 'True' so only - one machine is actually processing events, but if one goes - down there are other that can take place - - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1-n machines - - order of events is not important - - 'sequential' should be 'False' + This will completely remove project from server without any step back. Args: - source_topic (Union[str, list[str]]): Source topic to enroll with - wildcards '*', or explicit list of topics. - target_topic (str): Topic of dependent event. - sender (str): Identifier of sender (e.g. service name or username). - description (Optional[str]): Human readable text shown - in target event. - sequential (Optional[bool]): The source topic must be processed - in sequence. - events_filter (Optional[dict[str, Any]]): Filtering conditions - to filter the source event. For more technical specifications - look to server backed 'ayon_server.sqlfilter.Filter'. - TODO: Add example of filters. - max_retries (Optional[int]): How many times can be event retried. - Default value is based on server (3 at the time of this PR). - ignore_older_than (Optional[int]): Ignore events older than - given number in days. - ignore_sender_types (Optional[list[str]]): Ignore events triggered - by given sender types. - - Returns: - Optional[dict[str, Any]]: None if there is no event matching - filters. Created event with 'target_topic'. + project_name (str): Project name that will be removed. """ con = get_server_api_connection() - return con.enroll_event_job( - source_topic=source_topic, - target_topic=target_topic, - sender=sender, - description=description, - sequential=sequential, - events_filter=events_filter, - max_retries=max_retries, - ignore_older_than=ignore_older_than, - ignore_sender_types=ignore_sender_types, + return con.delete_project( + project_name=project_name, ) @@ -5284,1215 +5245,1254 @@ def delete_folder( Args: project_name (str): Project name. folder_id (str): Folder id to delete. - force (Optional[bool]): Folder delete folder with all children - folder, products, versions and representations. - - """ - con = get_server_api_connection() - return con.delete_folder( - project_name=project_name, - folder_id=folder_id, - force=force, - ) - - -def get_full_link_type_name( - link_type_name: str, - input_type: str, - output_type: str, -) -> str: - """Calculate full link type name used for query from server. - - Args: - link_type_name (str): Type of link. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Returns: - str: Full name of link type used for query from server. - - """ - con = get_server_api_connection() - return con.get_full_link_type_name( - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - ) - - -def get_link_types( - project_name: str, -) -> list[dict[str, Any]]: - """All link types available on a project. - - Example output: - [ - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - ] - - Args: - project_name (str): Name of project where to look for link types. - - Returns: - list[dict[str, Any]]: Link types available on project. - - """ - con = get_server_api_connection() - return con.get_link_types( - project_name=project_name, - ) - - -def get_link_type( - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -) -> Optional[dict[str, Any]]: - """Get link type data. - - There is not dedicated REST endpoint to get single link type, - so method 'get_link_types' is used. - - Example output: - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - - Args: - project_name (str): Project where link type is available. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Returns: - Optional[dict[str, Any]]: Link type information. - - """ - con = get_server_api_connection() - return con.get_link_type( - project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - ) - - -def create_link_type( - project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[dict[str, Any]] = None, -): - """Create or update link type on server. - - Warning: - Because PUT is used for creation it is also used for update. - - Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Additional data related to link. - - Raises: - HTTPRequestError: Server error happened. + force (Optional[bool]): Folder delete folder with all children + folder, products, versions and representations. """ con = get_server_api_connection() - return con.create_link_type( + return con.delete_folder( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + folder_id=folder_id, + force=force, ) -def delete_link_type( +def get_rest_task( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -): - """Remove link type from project. - - Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - - Raises: - HTTPRequestError: Server error happened. - - """ + task_id: str, +) -> Optional["TaskDict"]: con = get_server_api_connection() - return con.delete_link_type( + return con.get_rest_task( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + task_id=task_id, ) -def make_sure_link_type_exists( +def get_tasks( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[dict[str, Any]] = None, -): - """Make sure link type exists on a project. + task_ids: Optional[Iterable[str]] = None, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Generator["TaskDict", None, None]: + """Query task entities from server. Args: project_name (str): Name of project. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Link type related data. + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[TaskDict, None, None]: Queried task entities. """ con = get_server_api_connection() - return con.make_sure_link_type_exists( + return con.get_tasks( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + task_ids=task_ids, + task_names=task_names, + task_types=task_types, + folder_ids=folder_ids, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def create_link( +def get_task_by_name( project_name: str, - link_type_name: str, - input_id: str, - input_type: str, - output_id: str, - output_type: str, - link_name: Optional[str] = None, -): - """Create link between 2 entities. - - Link has a type which must already exists on a project. - - Example output:: - - { - "id": "59a212c0d2e211eda0e20242ac120002" - } + folder_id: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by name and folder id. Args: - project_name (str): Project where the link is created. - link_type_name (str): Type of link. - input_id (str): Input entity id. - input_type (str): Entity type of input entity. - output_id (str): Output entity id. - output_type (str): Entity type of output entity. - link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + task_name (str): Task name + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - dict[str, str]: Information about link. - - Raises: - HTTPRequestError: Server error happened. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.create_link( + return con.get_task_by_name( project_name=project_name, - link_type_name=link_type_name, - input_id=input_id, - input_type=input_type, - output_id=output_id, - output_type=output_type, - link_name=link_name, + folder_id=folder_id, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, ) -def delete_link( +def get_task_by_id( project_name: str, - link_id: str, -): - """Remove link by id. + task_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by id. Args: - project_name (str): Project where link exists. - link_id (str): Id of link. + project_name (str): Name of project where to look for queried + entities. + task_id (str): Task id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. - Raises: - HTTPRequestError: Server error happened. + Returns: + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.delete_link( + return con.get_task_by_id( project_name=project_name, - link_id=link_id, + task_id=task_id, + fields=fields, + own_attributes=own_attributes, ) -def get_entities_links( +def get_tasks_by_folder_paths( project_name: str, - entity_type: str, - entity_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - link_names: Optional[Iterable[str]] = None, - link_name_regex: Optional[str] = None, -) -> dict[str, list[dict[str, Any]]]: - """Helper method to get links from server for entity types. - - .. highlight:: text - .. code-block:: text - - Example output: - { - "59a212c0d2e211eda0e20242ac120001": [ - { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" - }, - ... - ], - ... - } + folder_paths: Iterable[str], + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> dict[str, list["TaskDict"]]: + """Query task entities from server by folder paths. Args: - project_name (str): Project where links are. - entity_type (Literal["folder", "task", "product", - "version", "representations"]): Entity type. - entity_ids (Optional[Iterable[str]]): Ids of entities for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - link_names (Optional[Iterable[str]]): Link name filters. - link_name_regex (Optional[str]): Regex filter for link name. + project_name (str): Name of project. + folder_paths (list[str]): Folder paths. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - dict[str, list[dict[str, Any]]]: Link info by entity ids. + dict[str, list[TaskDict]]: Task entities by + folder path. """ con = get_server_api_connection() - return con.get_entities_links( + return con.get_tasks_by_folder_paths( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - link_types=link_types, - link_direction=link_direction, - link_names=link_names, - link_name_regex=link_name_regex, + folder_paths=folder_paths, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_folders_links( +def get_tasks_by_folder_path( project_name: str, - folder_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query folders links from server. + folder_path: str, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> list["TaskDict"]: + """Query task entities from server by folder path. Args: - project_name (str): Project where links are. - folder_ids (Optional[Iterable[str]]): Ids of folders for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by folder ids. + project_name (str): Name of project. + folder_path (str): Folder path. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. """ con = get_server_api_connection() - return con.get_folders_links( + return con.get_tasks_by_folder_path( project_name=project_name, - folder_ids=folder_ids, - link_types=link_types, - link_direction=link_direction, + folder_path=folder_path, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_folder_links( +def get_task_by_folder_path( project_name: str, - folder_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query folder links from server. + folder_path: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by folder path and task name. Args: - project_name (str): Project where links are. - folder_id (str): Folder id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + fields (Optional[Iterable[str]]): Task fields that should + be returned. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - list[dict[str, Any]]: Link info of folder. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_folder_links( + return con.get_task_by_folder_path( project_name=project_name, - folder_id=folder_id, - link_types=link_types, - link_direction=link_direction, + folder_path=folder_path, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, ) -def get_tasks_links( +def create_task( project_name: str, - task_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query tasks links from server. + name: str, + task_type: str, + folder_id: str, + label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, +) -> str: + """Create new task. Args: - project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name. + name (str): Folder name. + task_type (str): Task type. + folder_id (str): Parent folder id. + label (Optional[str]): Label of folder. + assignees (Optional[Iterable[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[Iterable[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is + generated. Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. + str: Task id. """ con = get_server_api_connection() - return con.get_tasks_links( + return con.create_task( project_name=project_name, - task_ids=task_ids, - link_types=link_types, - link_direction=link_direction, + name=name, + task_type=task_type, + folder_id=folder_id, + label=label, + assignees=assignees, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + task_id=task_id, ) -def get_task_links( +def update_task( project_name: str, task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query task links from server. - - Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of task. + name: Optional[str] = None, + task_type: Optional[str] = None, + folder_id: Optional[str] = None, + label: Optional[str] = NOT_SET, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, +): + """Update task entity on server. - """ - con = get_server_api_connection() - return con.get_task_links( - project_name=project_name, - task_id=task_id, - link_types=link_types, - link_direction=link_direction, - ) + Do not pass ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + Update of ``data`` will override existing value on folder entity. -def get_products_links( - project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query products links from server. + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. + project_name (str): Project name. + task_id (str): Task id. + name (Optional[str]): New name. + task_type (Optional[str]): New task type. + folder_id (Optional[str]): New folder id. + label (Optional[Optional[str]]): New label. + assignees (Optional[str]): New assignees. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.get_products_links( + return con.update_task( project_name=project_name, - product_ids=product_ids, - link_types=link_types, - link_direction=link_direction, + task_id=task_id, + name=name, + task_type=task_type, + folder_id=folder_id, + label=label, + assignees=assignees, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, ) -def get_product_links( +def delete_task( project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query product links from server. + task_id: str, +): + """Delete task. Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of product. + project_name (str): Project name. + task_id (str): Task id to delete. """ con = get_server_api_connection() - return con.get_product_links( + return con.delete_task( project_name=project_name, - product_id=product_id, - link_types=link_types, - link_direction=link_direction, + task_id=task_id, ) -def get_versions_links( +def get_rest_product( project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query versions links from server. - - Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. - - """ + product_id: str, +) -> Optional["ProductDict"]: con = get_server_api_connection() - return con.get_versions_links( + return con.get_rest_product( project_name=project_name, - version_ids=version_ids, - link_types=link_types, - link_direction=link_direction, + product_id=product_id, ) -def get_version_links( +def get_products( project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query version links from server. + product_ids: Optional[Iterable[str]] = None, + product_names: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + product_types: Optional[Iterable[str]] = None, + product_name_regex: Optional[str] = None, + product_path_regex: Optional[str] = None, + names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["ProductDict", None, None]: + """Query products from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project. + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for + filtering. + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for + filtering. + product_name_regex (Optional[str]): Filter products by name regex. + product_path_regex (Optional[str]): Filter products by path regex. + Path starts with folder path and ends with product name. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + statuses (Optional[Iterable[str]]): Product statuses used + for filtering. + tags (Optional[Iterable[str]]): Product tags used + for filtering. + active (Optional[bool]): Filter active/inactive products. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - list[dict[str, Any]]: Link info of version. + Generator[ProductDict, None, None]: Queried product entities. """ con = get_server_api_connection() - return con.get_version_links( + return con.get_products( project_name=project_name, - version_id=version_id, - link_types=link_types, - link_direction=link_direction, + product_ids=product_ids, + product_names=product_names, + folder_ids=folder_ids, + product_types=product_types, + product_name_regex=product_name_regex, + product_path_regex=product_path_regex, + names_by_folder_ids=names_by_folder_ids, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_representations_links( +def get_product_by_id( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query representations links from server. + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["ProductDict"]: + """Query product entity by id. Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where to look for queried + entities. + product_id (str): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_representations_links( + return con.get_product_by_id( project_name=project_name, - representation_ids=representation_ids, - link_types=link_types, - link_direction=link_direction, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def get_representation_links( +def get_product_by_name( project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query representation links from server. + product_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["ProductDict"]: + """Query product entity by name and folder id. Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where to look for queried + entities. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - list[dict[str, Any]]: Link info of representation. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_representation_links( + return con.get_product_by_name( project_name=project_name, - representation_id=representation_id, - link_types=link_types, - link_direction=link_direction, + product_name=product_name, + folder_id=folder_id, + fields=fields, + own_attributes=own_attributes, ) -def get_entity_lists( - project_name: str, - *, - list_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = None, +def get_product_types( fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: - """Fetch entity lists from server. +) -> list["ProductTypeDict"]: + """Types of products. + + This is server wide information. Product types have 'name', 'icon' and + 'color'. Args: - project_name (str): Project name where entity lists are. - list_ids (Optional[Iterable[str]]): List of entity list ids to - fetch. - active (Optional[bool]): Filter by active state of entity lists. - fields (Optional[Iterable[str]]): Fields to fetch from server. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - Generator[Dict[str, Any], None, None]: Entity list entities - matching defined filters. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_entity_lists( - project_name=project_name, - list_ids=list_ids, - active=active, + return con.get_product_types( fields=fields, ) -def get_entity_list_rest( +def get_project_product_types( project_name: str, - list_id: str, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using REST API. + fields: Optional[Iterable[str]] = None, +) -> list["ProductTypeDict"]: + """DEPRECATED Types of products available in a project. + + Filter only product types available in a project. Args: - project_name (str): Project name. - list_id (str): Entity list id. + project_name (str): Name of the project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_entity_list_rest( + return con.get_project_product_types( project_name=project_name, - list_id=list_id, + fields=fields, ) -def get_entity_list_by_id( - project_name: str, - list_id: str, - fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using GraphQl. +def get_product_type_names( + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """DEPRECATED Product type names. + + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. Args: - project_name (str): Project name. - list_id (str): Entity list id. - fields (Optional[Iterable[str]]): Fields to fetch from server. + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + set[str]: Product type names. """ con = get_server_api_connection() - return con.get_entity_list_by_id( + return con.get_product_type_names( project_name=project_name, - list_id=list_id, - fields=fields, + product_ids=product_ids, ) -def create_entity_list( +def create_product( project_name: str, - entity_type: "EntityListEntityType", - label: str, - *, - list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, - owner: Optional[str] = None, + name: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, - list_id: Optional[str] = None, + product_id: Optional[str] = None, ) -> str: - """Create entity list. + """Create new product. Args: - project_name (str): Project name where entity list lives. - entity_type (EntityListEntityType): Which entity types can be - used in list. - label (str): Entity list label. - list_type (Optional[str]): Entity list type. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - template (Optional[dict[str, Any]]): Dynamic list template. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - items (Optional[list[dict[str, Any]]]): Initial items in - entity list. - list_id (Optional[str]): Entity list id. + project_name (str): Project name. + name (str): Product name. + product_type (str): Product type. + folder_id (str): Parent folder id. + attrib (Optional[dict[str, Any]]): Product attributes. + data (Optional[dict[str, Any]]): Product data. + tags (Optional[Iterable[str]]): Product tags. + status (Optional[str]): Product status. + active (Optional[bool]): Product active state. + product_id (Optional[str]): Product id. If not passed new id is + generated. + + Returns: + str: Product id. """ con = get_server_api_connection() - return con.create_entity_list( + return con.create_product( project_name=project_name, - entity_type=entity_type, - label=label, - list_type=list_type, - access=access, + name=name, + product_type=product_type, + folder_id=folder_id, attrib=attrib, data=data, tags=tags, - template=template, - owner=owner, + status=status, active=active, - items=items, - list_id=list_id, + product_id=product_id, ) -def update_entity_list( +def update_product( project_name: str, - list_id: str, - *, - label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - owner: Optional[str] = None, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, active: Optional[bool] = None, -) -> None: - """Update entity list. +): + """Update product entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id that will be updated. - label (Optional[str]): New label of entity list. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. + project_name (str): Project name. + product_id (str): Product id. + name (Optional[str]): New product name. + folder_id (Optional[str]): New product id. + product_type (Optional[str]): New product type. + attrib (Optional[dict[str, Any]]): New product attributes. + data (Optional[dict[str, Any]]): New product data. + tags (Optional[Iterable[str]]): New product tags. + status (Optional[str]): New product status. + active (Optional[bool]): New product active state. """ con = get_server_api_connection() - return con.update_entity_list( + return con.update_product( project_name=project_name, - list_id=list_id, - label=label, - access=access, + product_id=product_id, + name=name, + folder_id=folder_id, + product_type=product_type, attrib=attrib, data=data, tags=tags, - owner=owner, + status=status, active=active, ) -def delete_entity_list( - project_name: str, - list_id: str, -) -> None: - """Delete entity list from project. - - Args: - project_name (str): Project name. - list_id (str): Entity list id that will be removed. - - """ - con = get_server_api_connection() - return con.delete_entity_list( - project_name=project_name, - list_id=list_id, - ) - - -def get_entity_list_attribute_definitions( +def delete_product( project_name: str, - list_id: str, -) -> List["EntityListAttributeDefinitionDict"]: - """Get attribute definitioins on entity list. + product_id: str, +): + """Delete product. Args: project_name (str): Project name. - list_id (str): Entity list id. - - Returns: - List[EntityListAttributeDefinitionDict]: List of attribute - definitions. + product_id (str): Product id to delete. """ con = get_server_api_connection() - return con.get_entity_list_attribute_definitions( + return con.delete_product( project_name=project_name, - list_id=list_id, + product_id=product_id, ) -def set_entity_list_attribute_definitions( +def get_rest_version( project_name: str, - list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], -) -> None: - """Set attribute definitioins on entity list. - - Args: - project_name (str): Project name. - list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): - List of attribute definitions. - - """ + version_id: str, +) -> Optional["VersionDict"]: con = get_server_api_connection() - return con.set_entity_list_attribute_definitions( + return con.get_rest_version( project_name=project_name, - list_id=list_id, - attribute_definitions=attribute_definitions, + version_id=version_id, ) -def create_entity_list_item( +def get_versions( project_name: str, - list_id: str, - *, - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - item_id: Optional[str] = None, -) -> str: - """Create entity list item. - - Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Item attribute values. - data (Optional[dict[str, Any]]): Item data. - tags (Optional[list[str]]): Tags of item in entity list. - item_id (Optional[str]): Id of item that will be created. + version_ids: Optional[Iterable[str]] = None, + product_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + versions: Optional[Iterable[str]] = None, + hero: bool = True, + standard: bool = True, + latest: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["VersionDict", None, None]: + """Get version entities based on passed filters from server. + + Args: + project_name (str): Name of project where to look for versions. + version_ids (Optional[Iterable[str]]): Version ids used for + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + task_ids (Optional[Iterable[str]]): Task ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Skip hero versions when set to False. + standard (Optional[bool]): Skip standard (non-hero) when + set to False. + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute + set to True. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields to be queried + for version. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - str: Item id. + Generator[VersionDict, None, None]: Queried version entities. """ con = get_server_api_connection() - return con.create_entity_list_item( + return con.get_versions( project_name=project_name, - list_id=list_id, - position=position, - label=label, - attrib=attrib, - data=data, + version_ids=version_ids, + product_ids=product_ids, + task_ids=task_ids, + versions=versions, + hero=hero, + standard=standard, + latest=latest, + statuses=statuses, tags=tags, - item_id=item_id, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def update_entity_list_items( +def get_version_by_id( project_name: str, - list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", -) -> None: - """Update items in entity list. + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query version entity by id. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. - mode (EntityListItemMode): Mode of items update. + project_name (str): Name of project where to look for queried + entities. + version_id (str): Version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.update_entity_list_items( + return con.get_version_by_id( project_name=project_name, - list_id=list_id, - items=items, - mode=mode, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def update_entity_list_item( +def get_version_by_name( project_name: str, - list_id: str, - item_id: str, - *, - new_list_id: Optional[str], - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, -) -> None: - """Update item in entity list. + version: int, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query version entity by version and product id. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id where item lives. - item_id (str): Item id that will be removed from entity list. - new_list_id (Optional[str]): New entity list id where item will be - added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Attributes of item in entity - list. - data (Optional[dict[str, Any]]): Custom data of item in - entity list. - tags (Optional[list[str]]): Tags of item in entity list. + project_name (str): Name of project where to look for queried + entities. + version (int): Version of version entity. + product_id (str): Product id. Product is a parent of version. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.update_entity_list_item( + return con.get_version_by_name( project_name=project_name, - list_id=list_id, - item_id=item_id, - new_list_id=new_list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, + version=version, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def delete_entity_list_item( +def get_hero_version_by_id( project_name: str, - list_id: str, - item_id: str, -) -> None: - """Delete item from entity list. + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query hero version entity by id. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id from which item will be removed. - item_id (str): Item id that will be removed from entity list. + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.delete_entity_list_item( + return con.get_hero_version_by_id( project_name=project_name, - list_id=list_id, - item_id=item_id, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def get_rest_project( +def get_hero_version_by_product_id( project_name: str, -) -> Optional["ProjectDict"]: - """Query project by name. + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query hero version entity by product id. - This call returns project with anatomy data. + Only one hero version is available on a product. Args: - project_name (str): Name of project. + project_name (str): Name of project where to look for queried + entities. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_rest_project( + return con.get_hero_version_by_product_id( project_name=project_name, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def get_rest_projects( +def get_hero_versions( + project_name: str, + product_ids: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, active: Optional[bool] = True, - library: Optional[bool] = None, -) -> Generator["ProjectDict", None, None]: - """Query available project entities. + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["VersionDict", None, None]: + """Query hero versions by multiple filters. - User must be logged in. + Only one hero version is available on a product. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. + project_name (str): Name of project where to look for queried + entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - Generator[ProjectDict, None, None]: Available projects. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_rest_projects( + return con.get_hero_versions( + project_name=project_name, + product_ids=product_ids, + version_ids=version_ids, active=active, - library=library, + fields=fields, + own_attributes=own_attributes, ) -def get_project_names( +def get_last_versions( + project_name: str, + product_ids: Iterable[str], active: Optional[bool] = True, - library: Optional[bool] = None, -) -> list[str]: - """Receive available project names. - - User must be logged in. + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> dict[str, Optional["VersionDict"]]: + """Query last version entities by product ids. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - list[str]: List of available project names. + dict[str, Optional[VersionDict]]: Last versions by product id. """ con = get_server_api_connection() - return con.get_project_names( + return con.get_last_versions( + project_name=project_name, + product_ids=product_ids, active=active, - library=library, + fields=fields, + own_attributes=own_attributes, ) -def get_projects( +def get_last_version_by_product_id( + project_name: str, + product_id: str, active: Optional[bool] = True, - library: Optional[bool] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["ProjectDict", None, None]: - """Get projects. + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query last version entity by product id. Args: - active (Optional[bool]): Filter active or inactive projects. - Filter is disabled when 'None' is passed. - library (Optional[bool]): Filter library projects. Filter is - disabled when 'None' is passed. + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - Generator[ProjectDict, None, None]: Queried projects. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_projects( + return con.get_last_version_by_product_id( + project_name=project_name, + product_id=product_id, active=active, - library=library, fields=fields, own_attributes=own_attributes, ) -def get_project( +def get_last_version_by_product_name( project_name: str, + product_name: str, + folder_id: str, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["ProjectDict"]: - """Get project. + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query last version entity by product name and folder id. Args: - project_name (str): Name of project. + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - Optional[ProjectDict]: Project entity data or None - if project was not found. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_project( + return con.get_last_version_by_product_name( project_name=project_name, + product_name=product_name, + folder_id=folder_id, + active=active, fields=fields, own_attributes=own_attributes, ) -def create_project( +def version_is_latest( project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, -) -> "ProjectDict": - """Create project using AYON settings. + version_id: str, +) -> bool: + """Is version latest from a product. - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. + Args: + project_name (str): Project where to look for representation. + version_id (str): Version id. - Entered project name must be unique and project must not exist yet. + Returns: + bool: Version is latest or not. - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. + """ + con = get_server_api_connection() + return con.version_is_latest( + project_name=project_name, + version_id=version_id, + ) - Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. - Raises: - ValueError: When project name already exists. +def create_version( + project_name: str, + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + version_id: Optional[str] = None, +) -> str: + """Create new version. + + Args: + project_name (str): Project name. + version (int): Version. + product_id (str): Parent product id. + task_id (Optional[str]): Parent task id. + author (Optional[str]): Version author. + attrib (Optional[dict[str, Any]]): Version attributes. + data (Optional[dict[str, Any]]): Version data. + tags (Optional[Iterable[str]]): Version tags. + status (Optional[str]): Version status. + active (Optional[bool]): Version active state. + thumbnail_id (Optional[str]): Version thumbnail id. + version_id (Optional[str]): Version id. If not passed new id is + generated. Returns: - ProjectDict: Created project entity. + str: Version id. """ con = get_server_api_connection() - return con.create_project( + return con.create_version( project_name=project_name, - project_code=project_code, - library_project=library_project, - preset_name=preset_name, + version=version, + product_id=product_id, + task_id=task_id, + author=author, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + version_id=version_id, ) -def update_project( +def update_version( project_name: str, - library: Optional[bool] = None, - folder_types: Optional[list[dict[str, Any]]] = None, - task_types: Optional[list[dict[str, Any]]] = None, - link_types: Optional[list[dict[str, Any]]] = None, - statuses: Optional[list[dict[str, Any]]] = None, - tags: Optional[list[dict[str, Any]]] = None, - config: Optional[dict[str, Any]] = None, + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + author: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes, + thumbnail_id: Optional[str] = NOT_SET, ): - """Update project entity on server. + """Update version entity on server. + + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. + project_name (str): Project name. + version_id (str): Version id. + version (Optional[int]): New version. + product_id (Optional[str]): New product id. + task_id (Optional[str]): New task id. + author (Optional[str]): New author username. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.update_project( + return con.update_version( project_name=project_name, - library=library, - folder_types=folder_types, - task_types=task_types, - link_types=link_types, - statuses=statuses, - tags=tags, - config=config, + version_id=version_id, + version=version, + product_id=product_id, + task_id=task_id, + author=author, attrib=attrib, data=data, + tags=tags, + status=status, active=active, - project_code=project_code, - **changes, + thumbnail_id=thumbnail_id, ) -def delete_project( +def delete_version( project_name: str, + version_id: str, ): - """Delete project from server. - - This will completely remove project from server without any step back. + """Delete version. Args: - project_name (str): Project name that will be removed. + project_name (str): Project name. + version_id (str): Version id to delete. """ con = get_server_api_connection() - return con.delete_project( + return con.delete_version( project_name=project_name, + version_id=version_id, ) diff --git a/ayon_api/_products.py b/ayon_api/_products.py new file mode 100644 index 000000000..eff1384f5 --- /dev/null +++ b/ayon_api/_products.py @@ -0,0 +1,501 @@ +from __future__ import annotations + +import collections +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ._base import _BaseServerAPI, _PLACEHOLDER +from .utils import ( + prepare_list_filters, + create_entity_id, +) +from .graphql_queries import ( + products_graphql_query, + product_types_query, +) + +if typing.TYPE_CHECKING: + from .typing import ProductDict, ProductTypeDict + + +class _ProductsAPI(_BaseServerAPI): + def get_rest_product( + self, project_name: str, product_id: str + ) -> Optional["ProductDict"]: + return self.get_rest_entity_by_id(project_name, "product", product_id) + + def get_products( + self, + project_name: str, + product_ids: Optional[Iterable[str]] = None, + product_names: Optional[Iterable[str]]=None, + folder_ids: Optional[Iterable[str]]=None, + product_types: Optional[Iterable[str]]=None, + product_name_regex: Optional[str] = None, + product_path_regex: Optional[str] = None, + names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Generator["ProductDict", None, None]: + """Query products from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. + + Args: + project_name (str): Name of project. + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for + filtering. + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for + filtering. + product_name_regex (Optional[str]): Filter products by name regex. + product_path_regex (Optional[str]): Filter products by path regex. + Path starts with folder path and ends with product name. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + statuses (Optional[Iterable[str]]): Product statuses used + for filtering. + tags (Optional[Iterable[str]]): Product tags used + for filtering. + active (Optional[bool]): Filter active/inactive products. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Generator[ProductDict, None, None]: Queried product entities. + + """ + if not project_name: + return + + # Prepare these filters before 'name_by_filter_ids' filter + filter_product_names = None + if product_names is not None: + filter_product_names = set(product_names) + if not filter_product_names: + return + + filter_folder_ids = None + if folder_ids is not None: + filter_folder_ids = set(folder_ids) + if not filter_folder_ids: + return + + # This will disable 'folder_ids' and 'product_names' filters + # - maybe could be enhanced in future? + if names_by_folder_ids is not None: + filter_product_names = set() + filter_folder_ids = set() + + for folder_id, names in names_by_folder_ids.items(): + if folder_id and names: + filter_folder_ids.add(folder_id) + filter_product_names |= set(names) + + if not filter_product_names or not filter_folder_ids: + return + + # Convert fields and add minimum required fields + if fields: + fields = set(fields) | {"id"} + self._prepare_fields("product", fields) + else: + fields = self.get_default_fields_for_type("product") + + if active is not None: + fields.add("active") + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for products. The" + " argument will be removed from function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + # Add 'name' and 'folderId' if 'names_by_folder_ids' filter is entered + if names_by_folder_ids: + fields.add("name") + fields.add("folderId") + + # Prepare filters for query + filters = { + "projectName": project_name + } + + if filter_folder_ids: + filters["folderIds"] = list(filter_folder_ids) + + if filter_product_names: + filters["productNames"] = list(filter_product_names) + + if not prepare_list_filters( + filters, + ("productIds", product_ids), + ("productTypes", product_types), + ("productStatuses", statuses), + ("productTags", tags), + ): + return + + for filter_key, filter_value in ( + ("productNameRegex", product_name_regex), + ("productPathRegex", product_path_regex), + ): + if filter_value: + filters[filter_key] = filter_value + + query = products_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + parsed_data = query.query(self) + + products = parsed_data.get("project", {}).get("products", []) + # Filter products by 'names_by_folder_ids' + if names_by_folder_ids: + products_by_folder_id = collections.defaultdict(list) + for product in products: + filtered_product = self._filter_product( + project_name, product, active + ) + if filtered_product is not None: + folder_id = filtered_product["folderId"] + products_by_folder_id[folder_id].append(filtered_product) + + for folder_id, names in names_by_folder_ids.items(): + for folder_product in products_by_folder_id[folder_id]: + if folder_product["name"] in names: + yield folder_product + + else: + for product in products: + filtered_product = self._filter_product( + project_name, product, active + ) + if filtered_product is not None: + yield filtered_product + + def get_product_by_id( + self, + project_name: str, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional["ProductDict"]: + """Query product entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_id (str): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Optional[ProductDict]: Product entity data or None + if was not found. + + """ + products = self.get_products( + project_name, + product_ids=[product_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for product in products: + return product + return None + + def get_product_by_name( + self, + project_name: str, + product_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional["ProductDict"]: + """Query product entity by name and folder id. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Optional[ProductDict]: Product entity data or None + if was not found. + + """ + products = self.get_products( + project_name, + product_names=[product_name], + folder_ids=[folder_id], + active=None, + fields=fields, + own_attributes=own_attributes + ) + for product in products: + return product + return None + + def get_product_types( + self, fields: Optional[Iterable[str]] = None + ) -> list["ProductTypeDict"]: + """Types of products. + + This is server wide information. Product types have 'name', 'icon' and + 'color'. + + Args: + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + list[ProductTypeDict]: Product types information. + + """ + if not fields: + fields = self.get_default_fields_for_type("productType") + + query = product_types_query(fields) + + parsed_data = query.query(self) + + return parsed_data.get("productTypes", []) + + def get_project_product_types( + self, project_name: str, fields: Optional[Iterable[str]] = None + ) -> list["ProductTypeDict"]: + """DEPRECATED Types of products available in a project. + + Filter only product types available in a project. + + Args: + project_name (str): Name of the project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. + + Returns: + list[ProductTypeDict]: Product types information. + + """ + warnings.warn( + "Used deprecated function 'get_project_product_types'." + " Use 'get_project' with 'productTypes' in 'fields' instead.", + DeprecationWarning, + stacklevel=2, + ) + if fields is None: + fields = {"productTypes"} + else: + fields = { + f"productTypes.{key}" + for key in fields + } + + project = self.get_project(project_name, fields=fields) + return project["productTypes"] + + def get_product_type_names( + self, + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, + ) -> set[str]: + """DEPRECATED Product type names. + + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. + + Args: + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. + + Returns: + set[str]: Product type names. + + """ + warnings.warn( + "Used deprecated function 'get_product_type_names'." + " Use 'get_product_types' or 'get_products' instead.", + DeprecationWarning, + stacklevel=2, + ) + if project_name: + if not product_ids: + return set() + products = self.get_products( + project_name, + product_ids=product_ids, + fields=["productType"], + active=None, + ) + return { + product["productType"] + for product in products + } + + return { + product_info["name"] + for product_info in self.get_product_types(project_name) + } + + def create_product( + self, + project_name: str, + name: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] =None, + status: Optional[str] = None, + active: Optional[bool] = None, + product_id: Optional[str] = None, + ) -> str: + """Create new product. + + Args: + project_name (str): Project name. + name (str): Product name. + product_type (str): Product type. + folder_id (str): Parent folder id. + attrib (Optional[dict[str, Any]]): Product attributes. + data (Optional[dict[str, Any]]): Product data. + tags (Optional[Iterable[str]]): Product tags. + status (Optional[str]): Product status. + active (Optional[bool]): Product active state. + product_id (Optional[str]): Product id. If not passed new id is + generated. + + Returns: + str: Product id. + + """ + if not product_id: + product_id = create_entity_id() + create_data = { + "id": product_id, + "name": name, + "productType": product_type, + "folderId": folder_id, + } + for key, value in ( + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/products", + **create_data + ) + response.raise_for_status() + return product_id + + def update_product( + self, + project_name: str, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ): + """Update product entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + product_id (str): Product id. + name (Optional[str]): New product name. + folder_id (Optional[str]): New product id. + product_type (Optional[str]): New product type. + attrib (Optional[dict[str, Any]]): New product attributes. + data (Optional[dict[str, Any]]): New product data. + tags (Optional[Iterable[str]]): New product tags. + status (Optional[str]): New product status. + active (Optional[bool]): New product active state. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("productType", product_type), + ("folderId", folder_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/products/{product_id}", + **update_data + ) + response.raise_for_status() + + def delete_product(self, project_name: str, product_id: str): + """Delete product. + + Args: + project_name (str): Project name. + product_id (str): Product id to delete. + + """ + response = self.delete( + f"projects/{project_name}/products/{product_id}" + ) + response.raise_for_status() + + def _filter_product( + self, + project_name: str, + product: "ProductDict", + active: Optional[bool], + ) -> Optional["ProductDict"]: + if active is not None and product["active"] is not active: + return None + + self._convert_entity_data(product) + + return product diff --git a/ayon_api/_tasks.py b/ayon_api/_tasks.py new file mode 100644 index 000000000..ed5f22fec --- /dev/null +++ b/ayon_api/_tasks.py @@ -0,0 +1,514 @@ +from __future__ import annotations + +import typing +from typing import Optional, Iterable, Generator, Any + +from ._base import _BaseServerAPI +from .utils import ( + prepare_list_filters, + fill_own_attribs, + create_entity_id, + NOT_SET, +) +from .graphql_queries import ( + tasks_graphql_query, + tasks_by_folder_paths_graphql_query, +) + +if typing.TYPE_CHECKING: + from .typing import TaskDict + + +class _TasksAPI(_BaseServerAPI): + def get_rest_task( + self, project_name: str, task_id: str + ) -> Optional["TaskDict"]: + return self.get_rest_entity_by_id(project_name, "task", task_id) + + def get_tasks( + self, + project_name: str, + task_ids: Optional[Iterable[str]] = None, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Generator["TaskDict", None, None]: + """Query task entities from server. + + Args: + project_name (str): Name of project. + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Generator[TaskDict, None, None]: Queried task entities. + + """ + if not project_name: + return + + filters = { + "projectName": project_name + } + if not prepare_list_filters( + filters, + ("taskIds", task_ids), + ("taskNames", task_names), + ("taskTypes", task_types), + ("folderIds", folder_ids), + ("taskAssigneesAny", assignees), + ("taskAssigneesAll", assignees_all), + ("taskStatuses", statuses), + ("taskTags", tags), + ): + return + + if not fields: + fields = self.get_default_fields_for_type("task") + else: + fields = set(fields) + self._prepare_fields("task", fields, own_attributes) + + if active is not None: + fields.add("active") + + query = tasks_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + for parsed_data in query.continuous_query(self): + for task in parsed_data["project"]["tasks"]: + if active is not None and active is not task["active"]: + continue + + self._convert_entity_data(task) + + if own_attributes: + fill_own_attribs(task) + yield task + + def get_task_by_name( + self, + project_name: str, + folder_id: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, + ) -> Optional["TaskDict"]: + """Query task entity by name and folder id. + + Args: + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + task_name (str): Task name + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. + + """ + for task in self.get_tasks( + project_name, + folder_ids=[folder_id], + task_names=[task_name], + active=None, + fields=fields, + own_attributes=own_attributes + ): + return task + return None + + def get_task_by_id( + self, + project_name: str, + task_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Optional["TaskDict"]: + """Query task entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + task_id (str): Task id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. + + """ + for task in self.get_tasks( + project_name, + task_ids=[task_id], + active=None, + fields=fields, + own_attributes=own_attributes + ): + return task + return None + + def get_tasks_by_folder_paths( + self, + project_name: str, + folder_paths: Iterable[str], + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> dict[str, list["TaskDict"]]: + """Query task entities from server by folder paths. + + Args: + project_name (str): Name of project. + folder_paths (list[str]): Folder paths. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + dict[str, list[TaskDict]]: Task entities by + folder path. + + """ + folder_paths = set(folder_paths) + if not project_name or not folder_paths: + return {} + + filters = { + "projectName": project_name, + "folderPaths": list(folder_paths), + } + if not prepare_list_filters( + filters, + ("taskNames", task_names), + ("taskTypes", task_types), + ("taskAssigneesAny", assignees), + ("taskAssigneesAll", assignees_all), + ("taskStatuses", statuses), + ("taskTags", tags), + ): + return {} + + if not fields: + fields = self.get_default_fields_for_type("task") + else: + fields = set(fields) + self._prepare_fields("task", fields, own_attributes) + + if active is not None: + fields.add("active") + + query = tasks_by_folder_paths_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + + output = { + folder_path: [] + for folder_path in folder_paths + } + for parsed_data in query.continuous_query(self): + for folder in parsed_data["project"]["folders"]: + folder_path = folder["path"] + for task in folder["tasks"]: + if active is not None and active is not task["active"]: + continue + + self._convert_entity_data(task) + + if own_attributes: + fill_own_attribs(task) + output[folder_path].append(task) + return output + + def get_tasks_by_folder_path( + self, + project_name: str, + folder_path: str, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> list["TaskDict"]: + """Query task entities from server by folder path. + + Args: + project_name (str): Name of project. + folder_path (str): Folder path. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + """ + return self.get_tasks_by_folder_paths( + project_name, + [folder_path], + task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes + )[folder_path] + + def get_task_by_folder_path( + self, + project_name: str, + folder_path: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False + ) -> Optional["TaskDict"]: + """Query task entity by folder path and task name. + + Args: + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + fields (Optional[Iterable[str]]): Task fields that should + be returned. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. + + """ + for task in self.get_tasks_by_folder_path( + project_name, + folder_path, + active=None, + task_names=[task_name], + fields=fields, + own_attributes=own_attributes, + ): + return task + return None + + def create_task( + self, + project_name: str, + name: str, + task_type: str, + folder_id: str, + label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, + ) -> str: + """Create new task. + + Args: + project_name (str): Project name. + name (str): Folder name. + task_type (str): Task type. + folder_id (str): Parent folder id. + label (Optional[str]): Label of folder. + assignees (Optional[Iterable[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[Iterable[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is + generated. + + Returns: + str: Task id. + + """ + if not task_id: + task_id = create_entity_id() + create_data = { + "id": task_id, + "name": name, + "taskType": task_type, + "folderId": folder_id, + } + for key, value in ( + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("assignees", assignees), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/tasks", + **create_data + ) + response.raise_for_status() + return task_id + + def update_task( + self, + project_name: str, + task_id: str, + name: Optional[str] = None, + task_type: Optional[str] = None, + folder_id: Optional[str] = None, + label: Optional[str] = NOT_SET, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ): + """Update task entity on server. + + Do not pass ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + task_id (str): Task id. + name (Optional[str]): New name. + task_type (Optional[str]): New task type. + folder_id (Optional[str]): New folder id. + label (Optional[Optional[str]]): New label. + assignees (Optional[str]): New assignees. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + + """ + update_data = {} + for key, value in ( + ("name", name), + ("taskType", task_type), + ("folderId", folder_id), + ("assignees", assignees), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("label", label), + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/tasks/{task_id}", + **update_data + ) + response.raise_for_status() + + def delete_task(self, project_name: str, task_id: str): + """Delete task. + + Args: + project_name (str): Project name. + task_id (str): Task id to delete. + + """ + response = self.delete( + f"projects/{project_name}/tasks/{task_id}" + ) + response.raise_for_status() \ No newline at end of file diff --git a/ayon_api/_versions.py b/ayon_api/_versions.py new file mode 100644 index 000000000..e805505a7 --- /dev/null +++ b/ayon_api/_versions.py @@ -0,0 +1,639 @@ +from __future__ import annotations + +import warnings +import typing +from typing import Optional, Iterable, Generator, Any + +from ._base import _BaseServerAPI, _PLACEHOLDER +from .utils import ( + NOT_SET, + create_entity_id, + prepare_list_filters, +) +from .graphql import GraphQlQuery +from .graphql_queries import versions_graphql_query + +if typing.TYPE_CHECKING: + from .typing import VersionDict + + +class _VersionsAPI(_BaseServerAPI): + def get_rest_version( + self, project_name: str, version_id: str + ) -> Optional["VersionDict"]: + return self.get_rest_entity_by_id(project_name, "version", version_id) + + def get_versions( + self, + project_name: str, + version_ids: Optional[Iterable[str]] = None, + product_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + versions: Optional[Iterable[str]] = None, + hero: bool = True, + standard: bool = True, + latest: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Generator["VersionDict", None, None]: + """Get version entities based on passed filters from server. + + Args: + project_name (str): Name of project where to look for versions. + version_ids (Optional[Iterable[str]]): Version ids used for + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + task_ids (Optional[Iterable[str]]): Task ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Skip hero versions when set to False. + standard (Optional[bool]): Skip standard (non-hero) when + set to False. + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute + set to True. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields to be queried + for version. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Generator[VersionDict, None, None]: Queried version entities. + + """ + if not fields: + fields = self.get_default_fields_for_type("version") + else: + fields = set(fields) + self._prepare_fields("version", fields) + + # Make sure fields have minimum required fields + fields |= {"id", "version"} + + if active is not None: + fields.add("active") + + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for versions. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning + ) + + if not hero and not standard: + return + + filters = { + "projectName": project_name + } + if not prepare_list_filters( + filters, + ("taskIds", task_ids), + ("versionIds", version_ids), + ("productIds", product_ids), + ("taskIds", task_ids), + ("versions", versions), + ("versionStatuses", statuses), + ("versionTags", tags), + ): + return + + queries = [] + # Add filters based on 'hero' and 'standard' + # NOTE: There is not a filter to "ignore" hero versions or to get + # latest and hero version + # - if latest and hero versions should be returned it must be done in + # 2 graphql queries + if standard and not latest: + # This query all versions standard + hero + # - hero must be filtered out if is not enabled during loop + query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + query.set_variable_value(attr, filter_value) + queries.append(query) + else: + if hero: + # Add hero query if hero is enabled + hero_query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + hero_query.set_variable_value(attr, filter_value) + + hero_query.set_variable_value("heroOnly", True) + queries.append(hero_query) + + if standard: + standard_query = versions_graphql_query(fields) + for attr, filter_value in filters.items(): + standard_query.set_variable_value(attr, filter_value) + + if latest: + standard_query.set_variable_value("latestOnly", True) + queries.append(standard_query) + + for query in queries: + for parsed_data in query.continuous_query(self): + for version in parsed_data["project"]["versions"]: + if active is not None and version["active"] is not active: + continue + + if not hero and version["version"] < 0: + continue + + self._convert_entity_data(version) + + yield version + + def get_version_by_id( + self, + project_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional["VersionDict"]: + """Query version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (str): Version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_versions( + project_name, + version_ids={version_id}, + active=None, + hero=True, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_version_by_name( + self, + project_name: str, + version: int, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional["VersionDict"]: + """Query version entity by version and product id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version (int): Version of version entity. + product_id (str): Product id. Product is a parent of version. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_versions( + project_name, + product_ids={product_id}, + versions={version}, + active=None, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_version_by_id( + self, + project_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional["VersionDict"]: + """Query hero version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_hero_versions( + project_name, + version_ids=[version_id], + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_version_by_product_id( + self, + project_name: str, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER + ) -> Optional["VersionDict"]: + """Query hero version entity by product id. + + Only one hero version is available on a product. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + versions = self.get_hero_versions( + project_name, + product_ids=[product_id], + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_hero_versions( + self, + project_name: str, + product_ids: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator["VersionDict", None, None]: + """Query hero versions by multiple filters. + + Only one hero version is available on a product. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ + return self.get_versions( + project_name, + version_ids=version_ids, + product_ids=product_ids, + hero=True, + standard=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + + def get_last_versions( + self, + project_name: str, + product_ids: Iterable[str], + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> dict[str, Optional["VersionDict"]]: + """Query last version entities by product ids. + + Args: + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + dict[str, Optional[VersionDict]]: Last versions by product id. + + """ + if fields: + fields = set(fields) + fields.add("productId") + product_ids = set(product_ids) + versions = self.get_versions( + project_name, + product_ids=product_ids, + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + output = { + version["productId"]: version + for version in versions + } + for product_id in product_ids: + output.setdefault(product_id, None) + return output + + def get_last_version_by_product_id( + self, + project_name: str, + product_id: str, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional["VersionDict"]: + """Query last version entity by product id. + + Args: + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Queried version entity or None. + + """ + versions = self.get_versions( + project_name, + product_ids=[product_id], + latest=True, + hero=False, + active=active, + fields=fields, + own_attributes=own_attributes + ) + for version in versions: + return version + return None + + def get_last_version_by_product_name( + self, + project_name: str, + product_name: str, + folder_id: str, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional["VersionDict"]: + """Query last version entity by product name and folder id. + + Args: + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[VersionDict]: Queried version entity or None. + + """ + if not folder_id: + return None + + product = self.get_product_by_name( + project_name, product_name, folder_id, fields={"id"} + ) + if not product: + return None + return self.get_last_version_by_product_id( + project_name, + product["id"], + active=active, + fields=fields, + own_attributes=own_attributes + ) + + def version_is_latest(self, project_name: str, version_id: str) -> bool: + """Is version latest from a product. + + Args: + project_name (str): Project where to look for representation. + version_id (str): Version id. + + Returns: + bool: Version is latest or not. + + """ + query = GraphQlQuery("VersionIsLatest") + project_name_var = query.add_variable( + "projectName", "String!", project_name + ) + version_id_var = query.add_variable( + "versionId", "String!", version_id + ) + project_query = query.add_field("project") + project_query.set_filter("name", project_name_var) + version_query = project_query.add_field("version") + version_query.set_filter("id", version_id_var) + product_query = version_query.add_field("product") + latest_version_query = product_query.add_field("latestVersion") + latest_version_query.add_field("id") + + parsed_data = query.query(self) + latest_version = ( + parsed_data["project"]["version"]["product"]["latestVersion"] + ) + return latest_version["id"] == version_id + + def create_version( + self, + project_name: str, + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + version_id: Optional[str] = None, + ) -> str: + """Create new version. + + Args: + project_name (str): Project name. + version (int): Version. + product_id (str): Parent product id. + task_id (Optional[str]): Parent task id. + author (Optional[str]): Version author. + attrib (Optional[dict[str, Any]]): Version attributes. + data (Optional[dict[str, Any]]): Version data. + tags (Optional[Iterable[str]]): Version tags. + status (Optional[str]): Version status. + active (Optional[bool]): Version active state. + thumbnail_id (Optional[str]): Version thumbnail id. + version_id (Optional[str]): Version id. If not passed new id is + generated. + + Returns: + str: Version id. + + """ + if not version_id: + version_id = create_entity_id() + create_data = { + "id": version_id, + "version": version, + "productId": product_id, + } + for key, value in ( + ("taskId", task_id), + ("author", author), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/versions", + **create_data + ) + response.raise_for_status() + return version_id + + def update_version( + self, + project_name: str, + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ): + """Update version entity on server. + + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + version_id (str): Version id. + version (Optional[int]): New version. + product_id (Optional[str]): New product id. + task_id (Optional[str]): New task id. + author (Optional[str]): New author username. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + + """ + update_data = {} + for key, value in ( + ("version", version), + ("productId", product_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("author", author), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("taskId", task_id), + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/versions/{version_id}", + **update_data + ) + response.raise_for_status() + + def delete_version(self, project_name: str, version_id: str): + """Delete version. + + Args: + project_name (str): Project name. + version_id (str): Version id to delete. + + """ + response = self.delete( + f"projects/{project_name}/versions/{version_id}" + ) + response.raise_for_status() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 4a2767255..80f7ccb80 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -11,7 +11,6 @@ import json import time import logging -import collections import platform import copy import uuid @@ -42,18 +41,8 @@ DEFAULT_USER_FIELDS, DEFAULT_ENTITY_LIST_FIELDS, ) -from .graphql import GraphQlQuery, INTROSPECTION_QUERY -from .graphql_queries import ( - product_types_query, - tasks_graphql_query, - tasks_by_folder_paths_graphql_query, - products_graphql_query, - versions_graphql_query, - representations_graphql_query, - representations_hierarchy_qraphql_query, - workfiles_info_graphql_query, - users_graphql_query, -) +from .graphql import INTROSPECTION_QUERY +from .graphql_queries import users_graphql_query from .exceptions import ( FailedOperations, UnauthorizedError, @@ -64,8 +53,6 @@ RequestType, RequestTypes, RestApiResponse, - RepresentationParents, - RepresentationHierarchy, prepare_query_string, logout_from_server, create_entity_id, @@ -79,21 +66,24 @@ get_media_mime_type, get_machine_name, fill_own_attribs, - prepare_list_filters, ) from ._base import _PLACEHOLDER from ._actions import _ActionsAPI from ._activities import _ActivitiesAPI from ._addons import _AddonsAPI from ._events import _EventsAPI -from ._folders import _FoldersAPI from ._links import _LinksAPI from ._lists import _ListsAPI from ._projects import _ProjectsAPI +from ._folders import _FoldersAPI +from ._tasks import _TasksAPI +from ._products import _ProductsAPI +from ._versions import _VersionsAPI from ._thumbnails import _ThumbnailsAPI from ._workfiles import _WorkfilesAPI from ._representations import _RepresentationsAPI + if typing.TYPE_CHECKING: from typing import Union from .typing import ( @@ -239,6 +229,9 @@ class ServerAPI( _EventsAPI, _ProjectsAPI, _FoldersAPI, + _TasksAPI, + _ProductsAPI, + _VersionsAPI, _RepresentationsAPI, _WorkfilesAPI, _LinksAPI, @@ -3425,1599 +3418,6 @@ def get_rest_entity_by_id( return response.data return None - def get_rest_task( - self, project_name: str, task_id: str - ) -> Optional["TaskDict"]: - return self.get_rest_entity_by_id(project_name, "task", task_id) - - def get_rest_product( - self, project_name: str, product_id: str - ) -> Optional["ProductDict"]: - return self.get_rest_entity_by_id(project_name, "product", product_id) - - def get_rest_version( - self, project_name: str, version_id: str - ) -> Optional["VersionDict"]: - return self.get_rest_entity_by_id(project_name, "version", version_id) - - def get_tasks( - self, - project_name: str, - task_ids: Optional[Iterable[str]] = None, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Generator["TaskDict", None, None]: - """Query task entities from server. - - Args: - project_name (str): Name of project. - task_ids (Iterable[str]): Task ids to filter. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Generator[TaskDict, None, None]: Queried task entities. - - """ - if not project_name: - return - - filters = { - "projectName": project_name - } - if not prepare_list_filters( - filters, - ("taskIds", task_ids), - ("taskNames", task_names), - ("taskTypes", task_types), - ("folderIds", folder_ids), - ("taskAssigneesAny", assignees), - ("taskAssigneesAll", assignees_all), - ("taskStatuses", statuses), - ("taskTags", tags), - ): - return - - if not fields: - fields = self.get_default_fields_for_type("task") - else: - fields = set(fields) - self._prepare_fields("task", fields, own_attributes) - - if active is not None: - fields.add("active") - - query = tasks_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - for parsed_data in query.continuous_query(self): - for task in parsed_data["project"]["tasks"]: - if active is not None and active is not task["active"]: - continue - - self._convert_entity_data(task) - - if own_attributes: - fill_own_attribs(task) - yield task - - def get_task_by_name( - self, - project_name: str, - folder_id: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, - ) -> Optional["TaskDict"]: - """Query task entity by name and folder id. - - Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - task_name (str): Task name - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[TaskDict]: Task entity data or None if was not found. - - """ - for task in self.get_tasks( - project_name, - folder_ids=[folder_id], - task_names=[task_name], - active=None, - fields=fields, - own_attributes=own_attributes - ): - return task - return None - - def get_task_by_id( - self, - project_name: str, - task_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Optional["TaskDict"]: - """Query task entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - task_id (str): Task id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[TaskDict]: Task entity data or None if was not found. - - """ - for task in self.get_tasks( - project_name, - task_ids=[task_id], - active=None, - fields=fields, - own_attributes=own_attributes - ): - return task - return None - - def get_tasks_by_folder_paths( - self, - project_name: str, - folder_paths: Iterable[str], - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Dict[str, List["TaskDict"]]: - """Query task entities from server by folder paths. - - Args: - project_name (str): Name of project. - folder_paths (list[str]): Folder paths. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Dict[str, List[TaskDict]]: Task entities by - folder path. - - """ - folder_paths = set(folder_paths) - if not project_name or not folder_paths: - return {} - - filters = { - "projectName": project_name, - "folderPaths": list(folder_paths), - } - if not prepare_list_filters( - filters, - ("taskNames", task_names), - ("taskTypes", task_types), - ("taskAssigneesAny", assignees), - ("taskAssigneesAll", assignees_all), - ("taskStatuses", statuses), - ("taskTags", tags), - ): - return {} - - if not fields: - fields = self.get_default_fields_for_type("task") - else: - fields = set(fields) - self._prepare_fields("task", fields, own_attributes) - - if active is not None: - fields.add("active") - - query = tasks_by_folder_paths_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - output = { - folder_path: [] - for folder_path in folder_paths - } - for parsed_data in query.continuous_query(self): - for folder in parsed_data["project"]["folders"]: - folder_path = folder["path"] - for task in folder["tasks"]: - if active is not None and active is not task["active"]: - continue - - self._convert_entity_data(task) - - if own_attributes: - fill_own_attribs(task) - output[folder_path].append(task) - return output - - def get_tasks_by_folder_path( - self, - project_name: str, - folder_path: str, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> List["TaskDict"]: - """Query task entities from server by folder path. - - Args: - project_name (str): Name of project. - folder_path (str): Folder path. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - """ - return self.get_tasks_by_folder_paths( - project_name, - [folder_path], - task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes - )[folder_path] - - def get_task_by_folder_path( - self, - project_name: str, - folder_path: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False - ) -> Optional["TaskDict"]: - """Query task entity by folder path and task name. - - Args: - project_name (str): Project name. - folder_path (str): Folder path. - task_name (str): Task name. - fields (Optional[Iterable[str]]): Task fields that should - be returned. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. - - Returns: - Optional[TaskDict]: Task entity data or None if was not found. - - """ - for task in self.get_tasks_by_folder_path( - project_name, - folder_path, - active=None, - task_names=[task_name], - fields=fields, - own_attributes=own_attributes, - ): - return task - return None - - def create_task( - self, - project_name: str, - name: str, - task_type: str, - folder_id: str, - label: Optional[str] = None, - assignees: Optional[Iterable[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - task_id: Optional[str] = None, - ) -> str: - """Create new task. - - Args: - project_name (str): Project name. - name (str): Folder name. - task_type (str): Task type. - folder_id (str): Parent folder id. - label (Optional[str]): Label of folder. - assignees (Optional[Iterable[str]]): Task assignees. - attrib (Optional[dict[str, Any]]): Task attributes. - data (Optional[dict[str, Any]]): Task data. - tags (Optional[Iterable[str]]): Task tags. - status (Optional[str]): Task status. - active (Optional[bool]): Task active state. - thumbnail_id (Optional[str]): Task thumbnail id. - task_id (Optional[str]): Task id. If not passed new id is - generated. - - Returns: - str: Task id. - - """ - if not task_id: - task_id = create_entity_id() - create_data = { - "id": task_id, - "name": name, - "taskType": task_type, - "folderId": folder_id, - } - for key, value in ( - ("label", label), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("assignees", assignees), - ("active", active), - ("thumbnailId", thumbnail_id), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/tasks", - **create_data - ) - response.raise_for_status() - return task_id - - def update_task( - self, - project_name: str, - task_id: str, - name: Optional[str] = None, - task_type: Optional[str] = None, - folder_id: Optional[str] = None, - label: Optional[str] = NOT_SET, - assignees: Optional[List[str]] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, - ): - """Update task entity on server. - - Do not pass ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - task_id (str): Task id. - name (Optional[str]): New name. - task_type (Optional[str]): New task type. - folder_id (Optional[str]): New folder id. - label (Optional[Union[str, None]]): New label. - assignees (Optional[str]): New assignees. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("taskType", task_type), - ("folderId", folder_id), - ("assignees", assignees), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - for key, value in ( - ("label", label), - ("thumbnailId", thumbnail_id), - ): - if value is not NOT_SET: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/tasks/{task_id}", - **update_data - ) - response.raise_for_status() - - def delete_task(self, project_name: str, task_id: str): - """Delete task. - - Args: - project_name (str): Project name. - task_id (str): Task id to delete. - - """ - response = self.delete( - f"projects/{project_name}/tasks/{task_id}" - ) - response.raise_for_status() - - def _filter_product( - self, - project_name: str, - product: "ProductDict", - active: "Union[bool, None]", - ) -> Optional["ProductDict"]: - if active is not None and product["active"] is not active: - return None - - self._convert_entity_data(product) - - return product - - def get_products( - self, - project_name: str, - product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]]=None, - folder_ids: Optional[Iterable[str]]=None, - product_types: Optional[Iterable[str]]=None, - product_name_regex: Optional[str] = None, - product_path_regex: Optional[str] = None, - names_by_folder_ids: Optional[Dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Generator["ProductDict", None, None]: - """Query products from server. - - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. - - Args: - project_name (str): Name of project. - product_ids (Optional[Iterable[str]]): Task ids to filter. - product_names (Optional[Iterable[str]]): Task names used for - filtering. - folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. - product_types (Optional[Iterable[str]]): Product types used for - filtering. - product_name_regex (Optional[str]): Filter products by name regex. - product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. - names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product - name filtering by folder id. - statuses (Optional[Iterable[str]]): Product statuses used - for filtering. - tags (Optional[Iterable[str]]): Product tags used - for filtering. - active (Optional[bool]): Filter active/inactive products. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Generator[ProductDict, None, None]: Queried product entities. - - """ - if not project_name: - return - - # Prepare these filters before 'name_by_filter_ids' filter - filter_product_names = None - if product_names is not None: - filter_product_names = set(product_names) - if not filter_product_names: - return - - filter_folder_ids = None - if folder_ids is not None: - filter_folder_ids = set(folder_ids) - if not filter_folder_ids: - return - - # This will disable 'folder_ids' and 'product_names' filters - # - maybe could be enhanced in future? - if names_by_folder_ids is not None: - filter_product_names = set() - filter_folder_ids = set() - - for folder_id, names in names_by_folder_ids.items(): - if folder_id and names: - filter_folder_ids.add(folder_id) - filter_product_names |= set(names) - - if not filter_product_names or not filter_folder_ids: - return - - # Convert fields and add minimum required fields - if fields: - fields = set(fields) | {"id"} - self._prepare_fields("product", fields) - else: - fields = self.get_default_fields_for_type("product") - - if active is not None: - fields.add("active") - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for products. The" - " argument will be removed from function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - # Add 'name' and 'folderId' if 'names_by_folder_ids' filter is entered - if names_by_folder_ids: - fields.add("name") - fields.add("folderId") - - # Prepare filters for query - filters = { - "projectName": project_name - } - - if filter_folder_ids: - filters["folderIds"] = list(filter_folder_ids) - - if filter_product_names: - filters["productNames"] = list(filter_product_names) - - if not prepare_list_filters( - filters, - ("productIds", product_ids), - ("productTypes", product_types), - ("productStatuses", statuses), - ("productTags", tags), - ): - return - - for filter_key, filter_value in ( - ("productNameRegex", product_name_regex), - ("productPathRegex", product_path_regex), - ): - if filter_value: - filters[filter_key] = filter_value - - query = products_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - parsed_data = query.query(self) - - products = parsed_data.get("project", {}).get("products", []) - # Filter products by 'names_by_folder_ids' - if names_by_folder_ids: - products_by_folder_id = collections.defaultdict(list) - for product in products: - filtered_product = self._filter_product( - project_name, product, active - ) - if filtered_product is not None: - folder_id = filtered_product["folderId"] - products_by_folder_id[folder_id].append(filtered_product) - - for folder_id, names in names_by_folder_ids.items(): - for folder_product in products_by_folder_id[folder_id]: - if folder_product["name"] in names: - yield folder_product - - else: - for product in products: - filtered_product = self._filter_product( - project_name, product, active - ) - if filtered_product is not None: - yield filtered_product - - def get_product_by_id( - self, - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["ProductDict"]: - """Query product entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_id (str): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Optional[ProductDict]: Product entity data or None - if was not found. - - """ - products = self.get_products( - project_name, - product_ids=[product_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for product in products: - return product - return None - - def get_product_by_name( - self, - project_name: str, - product_name: str, - folder_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["ProductDict"]: - """Query product entity by name and folder id. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_name (str): Product name. - folder_id (str): Folder id (Folder is a parent of products). - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Optional[ProductDict]: Product entity data or None - if was not found. - - """ - products = self.get_products( - project_name, - product_names=[product_name], - folder_ids=[folder_id], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for product in products: - return product - return None - - def get_product_types( - self, fields: Optional[Iterable[str]] = None - ) -> List["ProductTypeDict"]: - """Types of products. - - This is server wide information. Product types have 'name', 'icon' and - 'color'. - - Args: - fields (Optional[Iterable[str]]): Product types fields to query. - - Returns: - list[ProductTypeDict]: Product types information. - - """ - if not fields: - fields = self.get_default_fields_for_type("productType") - - query = product_types_query(fields) - - parsed_data = query.query(self) - - return parsed_data.get("productTypes", []) - - def get_project_product_types( - self, project_name: str, fields: Optional[Iterable[str]] = None - ) -> List["ProductTypeDict"]: - """DEPRECATED Types of products available in a project. - - Filter only product types available in a project. - - Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. - - Returns: - List[ProductTypeDict]: Product types information. - - """ - warnings.warn( - "Used deprecated function 'get_project_product_types'." - " Use 'get_project' with 'productTypes' in 'fields' instead.", - DeprecationWarning, - stacklevel=2, - ) - if fields is None: - fields = {"productTypes"} - else: - fields = { - f"productTypes.{key}" - for key in fields - } - - project = self.get_project(project_name, fields=fields) - return project["productTypes"] - - def get_product_type_names( - self, - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, - ) -> Set[str]: - """DEPRECATED Product type names. - - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. - - Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. - - Returns: - set[str]: Product type names. - - """ - warnings.warn( - "Used deprecated function 'get_product_type_names'." - " Use 'get_product_types' or 'get_products' instead.", - DeprecationWarning, - stacklevel=2, - ) - if project_name: - if not product_ids: - return set() - products = self.get_products( - project_name, - product_ids=product_ids, - fields=["productType"], - active=None, - ) - return { - product["productType"] - for product in products - } - - return { - product_info["name"] - for product_info in self.get_product_types(project_name) - } - - def create_product( - self, - project_name: str, - name: str, - product_type: str, - folder_id: str, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] =None, - status: Optional[str] = None, - active: "Union[bool, None]" = None, - product_id: Optional[str] = None, - ) -> str: - """Create new product. - - Args: - project_name (str): Project name. - name (str): Product name. - product_type (str): Product type. - folder_id (str): Parent folder id. - attrib (Optional[dict[str, Any]]): Product attributes. - data (Optional[dict[str, Any]]): Product data. - tags (Optional[Iterable[str]]): Product tags. - status (Optional[str]): Product status. - active (Optional[bool]): Product active state. - product_id (Optional[str]): Product id. If not passed new id is - generated. - - Returns: - str: Product id. - - """ - if not product_id: - product_id = create_entity_id() - create_data = { - "id": product_id, - "name": name, - "productType": product_type, - "folderId": folder_id, - } - for key, value in ( - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/products", - **create_data - ) - response.raise_for_status() - return product_id - - def update_product( - self, - project_name: str, - product_id: str, - name: Optional[str] = None, - folder_id: Optional[str] = None, - product_type: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - ): - """Update product entity on server. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - product_id (str): Product id. - name (Optional[str]): New product name. - folder_id (Optional[str]): New product id. - product_type (Optional[str]): New product type. - attrib (Optional[dict[str, Any]]): New product attributes. - data (Optional[dict[str, Any]]): New product data. - tags (Optional[Iterable[str]]): New product tags. - status (Optional[str]): New product status. - active (Optional[bool]): New product active state. - - """ - update_data = {} - for key, value in ( - ("name", name), - ("productType", product_type), - ("folderId", folder_id), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ): - if value is not None: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/products/{product_id}", - **update_data - ) - response.raise_for_status() - - def delete_product(self, project_name: str, product_id: str): - """Delete product. - - Args: - project_name (str): Project name. - product_id (str): Product id to delete. - - """ - response = self.delete( - f"projects/{project_name}/products/{product_id}" - ) - response.raise_for_status() - - def get_versions( - self, - project_name: str, - version_ids: Optional[Iterable[str]] = None, - product_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - versions: Optional[Iterable[str]] = None, - hero: bool = True, - standard: bool = True, - latest: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Generator["VersionDict", None, None]: - """Get version entities based on passed filters from server. - - Args: - project_name (str): Name of project where to look for versions. - version_ids (Optional[Iterable[str]]): Version ids used for - version filtering. - product_ids (Optional[Iterable[str]]): Product ids used for - version filtering. - task_ids (Optional[Iterable[str]]): Task ids used for - version filtering. - versions (Optional[Iterable[int]]): Versions we're interested in. - hero (Optional[bool]): Skip hero versions when set to False. - standard (Optional[bool]): Skip standard (non-hero) when - set to False. - latest (Optional[bool]): Return only latest version of standard - versions. This can be combined only with 'standard' attribute - set to True. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields to be queried - for version. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Generator[VersionDict, None, None]: Queried version entities. - - """ - if not fields: - fields = self.get_default_fields_for_type("version") - else: - fields = set(fields) - self._prepare_fields("version", fields) - - # Make sure fields have minimum required fields - fields |= {"id", "version"} - - if active is not None: - fields.add("active") - - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for versions. The" - " argument will be removed form function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - - if not hero and not standard: - return - - filters = { - "projectName": project_name - } - if not prepare_list_filters( - filters, - ("taskIds", task_ids), - ("versionIds", version_ids), - ("productIds", product_ids), - ("taskIds", task_ids), - ("versions", versions), - ("versionStatuses", statuses), - ("versionTags", tags), - ): - return - - queries = [] - # Add filters based on 'hero' and 'standard' - # NOTE: There is not a filter to "ignore" hero versions or to get - # latest and hero version - # - if latest and hero versions should be returned it must be done in - # 2 graphql queries - if standard and not latest: - # This query all versions standard + hero - # - hero must be filtered out if is not enabled during loop - query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - queries.append(query) - else: - if hero: - # Add hero query if hero is enabled - hero_query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): - hero_query.set_variable_value(attr, filter_value) - - hero_query.set_variable_value("heroOnly", True) - queries.append(hero_query) - - if standard: - standard_query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): - standard_query.set_variable_value(attr, filter_value) - - if latest: - standard_query.set_variable_value("latestOnly", True) - queries.append(standard_query) - - for query in queries: - for parsed_data in query.continuous_query(self): - for version in parsed_data["project"]["versions"]: - if active is not None and version["active"] is not active: - continue - - if not hero and version["version"] < 0: - continue - - self._convert_entity_data(version) - - yield version - - def get_version_by_id( - self, - project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query version entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version_id (str): Version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_versions( - project_name, - version_ids=[version_id], - active=None, - hero=True, - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_version_by_name( - self, - project_name: str, - version: int, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query version entity by version and product id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version (int): Version of version entity. - product_id (str): Product id. Product is a parent of version. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_versions( - project_name, - product_ids=[product_id], - versions=[version], - active=None, - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_hero_version_by_id( - self, - project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query hero version entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - version_id (int): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_hero_versions( - project_name, - version_ids=[version_id], - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_hero_version_by_product_id( - self, - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: - """Query hero version entity by product id. - - Only one hero version is available on a product. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_id (int): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - versions = self.get_hero_versions( - project_name, - product_ids=[product_id], - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_hero_versions( - self, - project_name: str, - product_ids: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Generator["VersionDict", None, None]: - """Query hero versions by multiple filters. - - Only one hero version is available on a product. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_ids (Optional[Iterable[str]]): Product ids. - version_ids (Optional[Iterable[str]]): Version ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. - - """ - return self.get_versions( - project_name, - version_ids=version_ids, - product_ids=product_ids, - hero=True, - standard=False, - active=active, - fields=fields, - own_attributes=own_attributes - ) - - def get_last_versions( - self, - project_name: str, - product_ids: Iterable[str], - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Dict[str, Optional["VersionDict"]]: - """Query last version entities by product ids. - - Args: - project_name (str): Project where to look for representation. - product_ids (Iterable[str]): Product ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - dict[str, Optional[VersionDict]]: Last versions by product id. - - """ - if fields: - fields = set(fields) - fields.add("productId") - product_ids = set(product_ids) - versions = self.get_versions( - project_name, - product_ids=product_ids, - latest=True, - hero=False, - active=active, - fields=fields, - own_attributes=own_attributes - ) - output = { - version["productId"]: version - for version in versions - } - for product_id in product_ids: - output.setdefault(product_id, None) - return output - - def get_last_version_by_product_id( - self, - project_name: str, - product_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["VersionDict"]: - """Query last version entity by product id. - - Args: - project_name (str): Project where to look for representation. - product_id (str): Product id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Queried version entity or None. - - """ - versions = self.get_versions( - project_name, - product_ids=[product_id], - latest=True, - hero=False, - active=active, - fields=fields, - own_attributes=own_attributes - ) - for version in versions: - return version - return None - - def get_last_version_by_product_name( - self, - project_name: str, - product_name: str, - folder_id: str, - active: "Union[bool, None]" = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, - ) -> Optional["VersionDict"]: - """Query last version entity by product name and folder id. - - Args: - project_name (str): Project where to look for representation. - product_name (str): Product name. - folder_id (str): Folder id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[VersionDict]: Queried version entity or None. - - """ - if not folder_id: - return None - - product = self.get_product_by_name( - project_name, product_name, folder_id, fields={"id"} - ) - if not product: - return None - return self.get_last_version_by_product_id( - project_name, - product["id"], - active=active, - fields=fields, - own_attributes=own_attributes - ) - - def version_is_latest(self, project_name: str, version_id: str) -> bool: - """Is version latest from a product. - - Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. - - Returns: - bool: Version is latest or not. - - """ - query = GraphQlQuery("VersionIsLatest") - project_name_var = query.add_variable( - "projectName", "String!", project_name - ) - version_id_var = query.add_variable( - "versionId", "String!", version_id - ) - project_query = query.add_field("project") - project_query.set_filter("name", project_name_var) - version_query = project_query.add_field("version") - version_query.set_filter("id", version_id_var) - product_query = version_query.add_field("product") - latest_version_query = product_query.add_field("latestVersion") - latest_version_query.add_field("id") - - parsed_data = query.query(self) - latest_version = ( - parsed_data["project"]["version"]["product"]["latestVersion"] - ) - return latest_version["id"] == version_id - - def create_version( - self, - project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, - ) -> str: - """Create new version. - - Args: - project_name (str): Project name. - version (int): Version. - product_id (str): Parent product id. - task_id (Optional[str]): Parent task id. - author (Optional[str]): Version author. - attrib (Optional[dict[str, Any]]): Version attributes. - data (Optional[dict[str, Any]]): Version data. - tags (Optional[Iterable[str]]): Version tags. - status (Optional[str]): Version status. - active (Optional[bool]): Version active state. - thumbnail_id (Optional[str]): Version thumbnail id. - version_id (Optional[str]): Version id. If not passed new id is - generated. - - Returns: - str: Version id. - - """ - if not version_id: - version_id = create_entity_id() - create_data = { - "id": version_id, - "version": version, - "productId": product_id, - } - for key, value in ( - ("taskId", task_id), - ("author", author), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ("thumbnailId", thumbnail_id), - ): - if value is not None: - create_data[key] = value - - response = self.post( - f"projects/{project_name}/versions", - **create_data - ) - response.raise_for_status() - return version_id - - def update_version( - self, - project_name: str, - version_id: str, - version: Optional[int] = None, - product_id: Optional[str] = None, - task_id: Optional[str] = NOT_SET, - author: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, - ): - """Update version entity on server. - - Do not pass ``task_id`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. - - Args: - project_name (str): Project name. - version_id (str): Version id. - version (Optional[int]): New version. - product_id (Optional[str]): New product id. - task_id (Optional[Union[str, None]]): New task id. - author (Optional[str]): New author username. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. - - """ - update_data = {} - for key, value in ( - ("version", version), - ("productId", product_id), - ("attrib", attrib), - ("data", data), - ("tags", tags), - ("status", status), - ("active", active), - ("author", author), - ): - if value is not None: - update_data[key] = value - - for key, value in ( - ("taskId", task_id), - ("thumbnailId", thumbnail_id), - ): - if value is not NOT_SET: - update_data[key] = value - - response = self.patch( - f"projects/{project_name}/versions/{version_id}", - **update_data - ) - response.raise_for_status() - - def delete_version(self, project_name: str, version_id: str): - """Delete version. - - Args: - project_name (str): Project name. - version_id (str): Version id to delete. - - """ - response = self.delete( - f"projects/{project_name}/versions/{version_id}" - ) - response.raise_for_status() - # --- Batch operations processing --- def send_batch_operations( self, From 81729e38795d5d8838182d3055bd015c03a04b81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:02:35 +0200 Subject: [PATCH 139/506] use set, list and dict for typehints --- ayon_api/server_api.py | 122 ++++++++++++++++++++--------------------- 1 file changed, 61 insertions(+), 61 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 80f7ccb80..32cc9e727 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -17,7 +17,7 @@ import warnings from contextlib import contextmanager import typing -from typing import Optional, Iterable, Tuple, Generator, Dict, List, Set, Any +from typing import Optional, Iterable, Tuple, Generator, Any import requests @@ -854,7 +854,7 @@ def _update_session_headers(self): elif key in self._session.headers: self._session.headers.pop(key) - def get_info(self) -> Dict[str, Any]: + def get_info(self) -> dict[str, Any]: """Get information about current used api key. By default, the 'info' contains only 'uptime' and 'version'. With @@ -924,7 +924,7 @@ def graphql_allows_traits_in_representations(self) -> bool: ) return self._graphql_allows_traits_in_representations - def _get_user_info(self) -> Optional[Dict[str, Any]]: + def _get_user_info(self) -> Optional[dict[str, Any]]: if self._access_token is None: return None @@ -953,7 +953,7 @@ def get_users( usernames: Optional[Iterable[str]] = None, emails: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: + ) -> Generator[dict[str, Any], None, None]: """Get Users. Only administrators and managers can fetch all users. For other users @@ -1028,7 +1028,7 @@ def get_user_by_name( username: str, project_name: Optional[str] = None, fields: Optional[Iterable[str]] = None, - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get user by name using GraphQl. Only administrators and managers can fetch all users. For other users @@ -1058,7 +1058,7 @@ def get_user_by_name( def get_user( self, username: Optional[str] = None - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get user info using REST endpoint. User contains only explicitly set attributes in 'attrib'. @@ -1067,7 +1067,7 @@ def get_user( username (Optional[str]): Username. Returns: - Optional[Dict[str, Any]]: User info or None if user is not + Optional[dict[str, Any]]: User info or None if user is not found. """ @@ -1090,7 +1090,7 @@ def get_user( def get_headers( self, content_type: Optional[str] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: if content_type is None: content_type = "application/json" @@ -1652,7 +1652,7 @@ def upload_reviewable( content_type: Optional[str] = None, filename: Optional[str] = None, progress: Optional[TransferProgress] = None, - headers: Optional[Dict[str, Any]] = None, + headers: Optional[dict[str, Any]] = None, **kwargs ) -> requests.Response: """Upload reviewable file to server. @@ -1667,7 +1667,7 @@ def upload_reviewable( filename (Optional[str]): User as original filename. Filename from 'filepath' is used when not filled. progress (Optional[TransferProgress]): Progress. - headers (Optional[Dict[str, Any]]): Headers. + headers (Optional[dict[str, Any]]): Headers. Returns: requests.Response: Server response. @@ -1729,7 +1729,7 @@ def trigger_server_restart(self): def query_graphql( self, query: str, - variables: Optional[Dict[str, Any]] = None, + variables: Optional[dict[str, Any]] = None, ) -> GraphQlResponse: """Execute GraphQl query. @@ -1751,10 +1751,10 @@ def query_graphql( response.raise_for_status() return GraphQlResponse(response) - def get_graphql_schema(self) -> Dict[str, Any]: + def get_graphql_schema(self) -> dict[str, Any]: return self.query_graphql(INTROSPECTION_QUERY).data["data"] - def get_server_schema(self) -> Optional[Dict[str, Any]]: + def get_server_schema(self) -> Optional[dict[str, Any]]: """Get server schema with info, url paths, components etc. Todos: @@ -1770,7 +1770,7 @@ def get_server_schema(self) -> Optional[Dict[str, Any]]: return response.data return None - def get_schemas(self) -> Dict[str, Any]: + def get_schemas(self) -> dict[str, Any]: """Get components schema. Name of components does not match entity type names e.g. 'project' is @@ -1805,7 +1805,7 @@ def set_attribute_config( self, attribute_name: str, data: "AttributeSchemaDataDict", - scope: List["AttributeScope"], + scope: list["AttributeScope"], position: Optional[int] = None, builtin: bool = False, ): @@ -1858,7 +1858,7 @@ def remove_attribute_config(self, attribute_name: str): def get_attributes_for_type( self, entity_type: "AttributeScope" - ) -> Dict[str, "AttributeSchemaDict"]: + ) -> dict[str, "AttributeSchemaDict"]: """Get attribute schemas available for an entity type. Example:: @@ -1911,7 +1911,7 @@ def get_attributes_for_type( def get_attributes_fields_for_type( self, entity_type: "AttributeScope" - ) -> Set[str]: + ) -> set[str]: """Prepare attribute fields for entity type. Returns: @@ -1924,7 +1924,7 @@ def get_attributes_fields_for_type( for attr in attributes } - def get_default_fields_for_type(self, entity_type: str) -> Set[str]: + def get_default_fields_for_type(self, entity_type: str) -> set[str]: """Default fields for entity type. Returns most of commonly used fields from server. @@ -2021,12 +2021,12 @@ def create_installer( version: str, python_version: str, platform_name: str, - python_modules: Dict[str, str], - runtime_python_modules: Dict[str, str], + python_modules: dict[str, str], + runtime_python_modules: dict[str, str], checksum: str, checksum_algorithm: str, file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, + sources: Optional[list[dict[str, Any]]] = None, ): """Create new installer information on server. @@ -2070,7 +2070,7 @@ def create_installer( response = self.post("desktop/installers", **body) response.raise_for_status() - def update_installer(self, filename: str, sources: List[Dict[str, Any]]): + def update_installer(self, filename: str, sources: list[dict[str, Any]]): """Update installer information on server. Args: @@ -2187,13 +2187,13 @@ def get_dependency_packages(self) -> "DependencyPackagesDict": def create_dependency_package( self, filename: str, - python_modules: Dict[str, str], - source_addons: Dict[str, str], + python_modules: dict[str, str], + source_addons: dict[str, str], installer_version: str, checksum: str, checksum_algorithm: str, file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, + sources: Optional[list[dict[str, Any]]] = None, platform_name: Optional[str] = None, ): """Create dependency package on server. @@ -2243,7 +2243,7 @@ def create_dependency_package( response.raise_for_status() def update_dependency_package( - self, filename: str, sources: List[Dict[str, Any]] + self, filename: str, sources: list[dict[str, Any]] ): """Update dependency package metadata on server. @@ -2402,15 +2402,15 @@ def get_bundles(self) -> "BundlesInfoDict": def create_bundle( self, name: str, - addon_versions: Dict[str, str], + addon_versions: dict[str, str], installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, dev_addons_config: Optional[ - Dict[str, "DevBundleAddonInfoDict"]] = None, + dict[str, "DevBundleAddonInfoDict"]] = None, ): """Create bundle on server. @@ -2473,15 +2473,15 @@ def create_bundle( def update_bundle( self, bundle_name: str, - addon_versions: Optional[Dict[str, str]] = None, + addon_versions: Optional[dict[str, str]] = None, installer_version: Optional[str] = None, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, dev_addons_config: Optional[ - Dict[str, "DevBundleAddonInfoDict"]] = None, + dict[str, "DevBundleAddonInfoDict"]] = None, ): """Update bundle on server. @@ -2531,16 +2531,16 @@ def update_bundle( def check_bundle_compatibility( self, name: str, - addon_versions: Dict[str, str], + addon_versions: dict[str, str], installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, dev_addons_config: Optional[ - Dict[str, "DevBundleAddonInfoDict"]] = None, - ) -> Dict[str, Any]: + dict[str, "DevBundleAddonInfoDict"]] = None, + ) -> dict[str, Any]: """Check bundle compatibility. Can be used as per-flight validation before creating bundle. @@ -2562,7 +2562,7 @@ def check_bundle_compatibility( dev addons. Can be used only if 'is_dev' is set to 'True'. Returns: - Dict[str, Any]: Server response, with 'success' and 'issues'. + dict[str, Any]: Server response, with 'success' and 'issues'. """ body = { @@ -2597,7 +2597,7 @@ def delete_bundle(self, bundle_name: str): response.raise_for_status() # Anatomy presets - def get_project_anatomy_presets(self) -> List["AnatomyPresetDict"]: + def get_project_anatomy_presets(self) -> list["AnatomyPresetDict"]: """Anatomy presets available on server. Content has basic information about presets. Example output:: @@ -2688,7 +2688,7 @@ def get_build_in_anatomy_preset(self) -> "AnatomyPresetDict": def get_project_root_overrides( self, project_name: str - ) -> Dict[str, Dict[str, str]]: + ) -> dict[str, dict[str, str]]: """Root overrides per site name. Method is based on logged user and can't be received for any other @@ -2709,7 +2709,7 @@ def get_project_root_overrides( def get_project_roots_by_site( self, project_name: str - ) -> Dict[str, Dict[str, str]]: + ) -> dict[str, dict[str, str]]: """Root overrides per site name. Method is based on logged user and can't be received for any other @@ -2739,7 +2739,7 @@ def get_project_roots_by_site( def get_project_root_overrides_by_site_id( self, project_name: str, site_id: Optional[str] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: """Root overrides for site. If site id is not passed a site set in current api object is used @@ -2765,7 +2765,7 @@ def get_project_root_overrides_by_site_id( def get_project_roots_for_site( self, project_name: str, site_id: Optional[str] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: """Root overrides for site. If site id is not passed a site set in current api object is used @@ -2798,7 +2798,7 @@ def _get_project_roots_values( project_name: str, site_id: Optional[str] = None, platform_name: Optional[str] = None, - ) -> Dict[str, str]: + ) -> dict[str, str]: """Root values for site or platform. Helper function that treats 'siteRoots' endpoint. The endpoint @@ -2840,7 +2840,7 @@ def _get_project_roots_values( def get_project_roots_by_site_id( self, project_name: str, site_id: Optional[str] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: """Root values for a site. If site id is not passed a site set in current api object is used @@ -2863,7 +2863,7 @@ def get_project_roots_by_site_id( def get_project_roots_by_platform( self, project_name: str, platform_name: Optional[str] = None - ) -> Dict[str, str]: + ) -> dict[str, str]: """Root values for a site. If platform name is not passed current platform name is used instead. @@ -2890,7 +2890,7 @@ def get_addon_settings_schema( addon_name: str, addon_version: str, project_name: Optional[str] = None - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Sudio/Project settings schema of an addon. Project schema may look differently as some enums are based on project @@ -2919,7 +2919,7 @@ def get_addon_settings_schema( def get_addon_site_settings_schema( self, addon_name: str, addon_version: str - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Site settings schema of an addon. Args: @@ -2941,7 +2941,7 @@ def get_addon_studio_settings( addon_name: str, addon_version: str, variant: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Addon studio settings. Receive studio settings for specific version of an addon. @@ -2975,7 +2975,7 @@ def get_addon_project_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Addon project settings. Receive project settings for specific version of an addon. The settings @@ -3029,7 +3029,7 @@ def get_addon_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Receive addon settings. Receive addon settings based on project name value. Some arguments may @@ -3067,7 +3067,7 @@ def get_addon_site_settings( addon_name: str, addon_version: str, site_id: Optional[str] = None, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Site settings of an addon. If site id is not available an empty dictionary is returned. @@ -3102,7 +3102,7 @@ def get_bundle_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Get complete set of settings for given data. If project is not passed then studio settings are returned. If variant @@ -3156,7 +3156,7 @@ def get_addons_studio_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """All addons settings in one bulk. Warnings: @@ -3202,7 +3202,7 @@ def get_addons_project_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Project settings of all addons. Server returns information about used addon versions, so full output @@ -3269,7 +3269,7 @@ def get_addons_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, - ) -> Dict[str, Any]: + ) -> dict[str, Any]: """Universal function to receive all addon settings. Based on 'project_name' will receive studio settings or project @@ -3314,7 +3314,7 @@ def get_addons_settings( only_values=only_values ) - def get_secrets(self) -> List["SecretDict"]: + def get_secrets(self) -> list["SecretDict"]: """Get all secrets. Example output:: @@ -3422,10 +3422,10 @@ def get_rest_entity_by_id( def send_batch_operations( self, project_name: str, - operations: List[Dict[str, Any]], + operations: list[dict[str, Any]], can_fail: bool = False, raise_on_fail: bool = True - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: """Post multiple CRUD operations to server. When multiple changes should be made on server side this is the best @@ -3461,10 +3461,10 @@ def send_batch_operations( def _send_batch_operations( self, uri: str, - operations: List[Dict[str, Any]], + operations: list[dict[str, Any]], can_fail: bool, raise_on_fail: bool - ) -> List[Dict[str, Any]]: + ) -> list[dict[str, Any]]: if not operations: return [] @@ -3527,7 +3527,7 @@ def _send_batch_operations( return op_results def _prepare_fields( - self, entity_type: str, fields: Set[str], own_attributes: bool = False + self, entity_type: str, fields: set[str], own_attributes: bool = False ): if not fields: return From dc058e0ac103a80accd8af04d561267dfe67cf88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:02:50 +0200 Subject: [PATCH 140/506] use list, dict and set in typing too --- ayon_api/typing.py | 130 ++++++++++++++++++++++----------------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 541ed734d..038372f0c 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -1,8 +1,8 @@ +from __future__ import annotations + import io from typing import ( Literal, - Dict, - List, Any, TypedDict, Union, @@ -49,7 +49,7 @@ EventFilterValueType = Union[ None, str, int, float, - List[str], List[int], List[float], + list[str], list[int], list[float], ] @@ -84,7 +84,7 @@ class EventFilterCondition(TypedDict): class EventFilter(TypedDict): - conditions: List[EventFilterCondition] + conditions: list[EventFilterCondition] operator: Literal["and", "or"] @@ -138,38 +138,38 @@ class AttributeSchemaDataDict(TypedDict): minItems: Optional[int] maxItems: Optional[int] regex: Optional[str] - enum: Optional[List[AttributeEnumItemDict]] + enum: Optional[list[AttributeEnumItemDict]] class AttributeSchemaDict(TypedDict): name: str position: int - scope: List[AttributeScope] + scope: list[AttributeScope] builtin: bool data: AttributeSchemaDataDict class AttributesSchemaDict(TypedDict): - attributes: List[AttributeSchemaDict] + attributes: list[AttributeSchemaDict] class AddonVersionInfoDict(TypedDict): hasSettings: bool hasSiteSettings: bool - frontendScopes: Dict[str, Any] - clientPyproject: Dict[str, Any] - clientSourceInfo: List[Dict[str, Any]] + frontendScopes: dict[str, Any] + clientPyproject: dict[str, Any] + clientSourceInfo: list[dict[str, Any]] isBroken: bool class AddonInfoDict(TypedDict): name: str title: str - versions: Dict[str, AddonVersionInfoDict] + versions: dict[str, AddonVersionInfoDict] class AddonsInfoDict(TypedDict): - addons: List[AddonInfoDict] + addons: list[AddonInfoDict] class InstallerInfoDict(TypedDict): @@ -178,15 +178,15 @@ class InstallerInfoDict(TypedDict): size: int checksum: str checksumAlgorithm: str - sources: List[Dict[str, Any]] + sources: list[dict[str, Any]] version: str pythonVersion: str - pythonModules: Dict[str, str] - runtimePythonModules: Dict[str, str] + pythonModules: dict[str, str] + runtimePythonModules: dict[str, str] class InstallersInfoDict(TypedDict): - installers: List[InstallerInfoDict] + installers: list[InstallerInfoDict] class DependencyPackageDict(TypedDict): @@ -195,14 +195,14 @@ class DependencyPackageDict(TypedDict): size: int checksum: str checksumAlgorithm: str - sources: List[Dict[str, Any]] + sources: list[dict[str, Any]] installerVersion: str - sourceAddons: Dict[str, str] - pythonModules: Dict[str, str] + sourceAddons: dict[str, str] + pythonModules: dict[str, str] class DependencyPackagesDict(TypedDict): - packages: List[DependencyPackageDict] + packages: list[DependencyPackageDict] class DevBundleAddonInfoDict(TypedDict): @@ -213,10 +213,10 @@ class DevBundleAddonInfoDict(TypedDict): class BundleInfoDict(TypedDict): name: str createdAt: str - addons: Dict[str, str] + addons: dict[str, str] installerVersion: str - dependencyPackages: Dict[str, str] - addonDevelopment: Dict[str, DevBundleAddonInfoDict] + dependencyPackages: dict[str, str] + addonDevelopment: dict[str, DevBundleAddonInfoDict] isProduction: bool isStaging: bool isArchived: bool @@ -225,9 +225,9 @@ class BundleInfoDict(TypedDict): class BundlesInfoDict(TypedDict): - bundles: List[BundleInfoDict] + bundles: list[BundleInfoDict] productionBundle: str - devBundles: List[str] + devBundles: list[str] class AnatomyPresetInfoDict(TypedDict): @@ -254,12 +254,12 @@ class AnatomyPresetTemplatesDict(TypedDict): version: str frame_padding: int frame: str - work: List[AnatomyPresetTemplateDict] - publish: List[AnatomyPresetTemplateDict] - hero: List[AnatomyPresetTemplateDict] - delivery: List[AnatomyPresetTemplateDict] - staging: List[AnatomyPresetTemplateDict] - others: List[AnatomyPresetTemplateDict] + work: list[AnatomyPresetTemplateDict] + publish: list[AnatomyPresetTemplateDict] + hero: list[AnatomyPresetTemplateDict] + delivery: list[AnatomyPresetTemplateDict] + staging: list[AnatomyPresetTemplateDict] + others: list[AnatomyPresetTemplateDict] class AnatomyPresetSubtypeDict(TypedDict): @@ -293,7 +293,7 @@ class AnatomyPresetStatusDict(TypedDict): state: str icon: str color: str - scope: List[StatusScope] + scope: list[StatusScope] original_name: str @@ -304,29 +304,29 @@ class AnatomyPresetTagDict(TypedDict): class AnatomyPresetDict(TypedDict): - roots: List[AnatomyPresetRootDict] + roots: list[AnatomyPresetRootDict] templates: AnatomyPresetTemplatesDict - attributes: Dict[str, Any] - folder_types: List[AnatomyPresetSubtypeDict] - task_types: List[AnatomyPresetSubtypeDict] - link_types: List[AnatomyPresetLinkTypeDict] - statuses: List[AnatomyPresetStatusDict] - tags: List[AnatomyPresetTagDict] + attributes: dict[str, Any] + folder_types: list[AnatomyPresetSubtypeDict] + task_types: list[AnatomyPresetSubtypeDict] + link_types: list[AnatomyPresetLinkTypeDict] + statuses: list[AnatomyPresetStatusDict] + tags: list[AnatomyPresetTagDict] class SecretDict(TypedDict): name: str value: str -ProjectDict = Dict[str, Any] -FolderDict = Dict[str, Any] -TaskDict = Dict[str, Any] -ProductDict = Dict[str, Any] -VersionDict = Dict[str, Any] -RepresentationDict = Dict[str, Any] -WorkfileInfoDict = Dict[str, Any] -EventDict = Dict[str, Any] -ActivityDict = Dict[str, Any] +ProjectDict = dict[str, Any] +FolderDict = dict[str, Any] +TaskDict = dict[str, Any] +ProductDict = dict[str, Any] +VersionDict = dict[str, Any] +RepresentationDict = dict[str, Any] +WorkfileInfoDict = dict[str, Any] +EventDict = dict[str, Any] +ActivityDict = dict[str, Any] AnyEntityDict = Union[ ProjectDict, FolderDict, @@ -344,16 +344,16 @@ class FlatFolderDict(TypedDict): id: str parentId: Optional[str] path: str - parents: List[str] + parents: list[str] name: str label: Optional[str] folderType: str hasTasks: bool hasChildren: bool - taskNames: List[str] + taskNames: list[str] status: str - attrib: Dict[str, Any] - ownAttrib: List[str] + attrib: dict[str, Any] + ownAttrib: list[str] updatedAt: str @@ -364,14 +364,14 @@ class ProjectHierarchyItemDict(TypedDict): status: str folderType: str hasTasks: bool - taskNames: List[str] - parents: List[str] + taskNames: list[str] + parents: list[str] parentId: Optional[str] - children: List["ProjectHierarchyItemDict"] + children: list["ProjectHierarchyItemDict"] class ProjectHierarchyDict(TypedDict): - hierarchy: List[ProjectHierarchyItemDict] + hierarchy: list[ProjectHierarchyItemDict] class ProductTypeDict(TypedDict): @@ -401,7 +401,7 @@ class ActionManifestDict(TypedDict): icon: Optional[IconDefType] adminOnly: bool managerOnly: bool - configFields: List[Dict[str, Any]] + configFields: list[dict[str, Any]] featured: bool addonName: str addonVersion: str @@ -444,7 +444,7 @@ class ActionQueryPayload(BaseActionPayload): class ActionFormPayload(BaseActionPayload): title: str - fields: List[Dict[str, Any]] + fields: list[dict[str, Any]] submit_label: str submit_icon: str cancel_label: str @@ -471,8 +471,8 @@ class ActionTriggerResponse(TypedDict): class ActionTakeResponse(TypedDict): eventId: str actionIdentifier: str - args: List[str] - context: Dict[str, Any] + args: list[str] + context: dict[str, Any] addonName: str addonVersion: str variant: str @@ -482,10 +482,10 @@ class ActionTakeResponse(TypedDict): class ActionConfigResponse(TypedDict): projectName: str entityType: str - entitySubtypes: List[str] - entityIds: List[str] - formData: Dict[str, Any] - value: Dict[str, Any] + entitySubtypes: list[str] + entityIds: list[str] + formData: dict[str, Any] + value: dict[str, Any] StreamType = Union[io.BytesIO, BinaryIO] @@ -493,4 +493,4 @@ class ActionConfigResponse(TypedDict): class EntityListAttributeDefinitionDict(TypedDict): name: str - data: Dict[str, Any] + data: dict[str, Any] From 99df5d9e26e91466c40654bcb0517da91f1dbc6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 12 Aug 2025 20:03:05 +0200 Subject: [PATCH 141/506] updated typehints in public api --- ayon_api/_api.py | 108 +++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 54 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 8ae32db4d..e30357663 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -666,7 +666,7 @@ def set_sender_type( ) -def get_info() -> Dict[str, Any]: +def get_info() -> dict[str, Any]: """Get information about current used api key. By default, the 'info' contains only 'uptime' and 'version'. With @@ -718,7 +718,7 @@ def get_users( usernames: Optional[Iterable[str]] = None, emails: Optional[Iterable[str]] = None, fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: +) -> Generator[dict[str, Any], None, None]: """Get Users. Only administrators and managers can fetch all users. For other users @@ -748,7 +748,7 @@ def get_user_by_name( username: str, project_name: Optional[str] = None, fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get user by name using GraphQl. Only administrators and managers can fetch all users. For other users @@ -775,7 +775,7 @@ def get_user_by_name( def get_user( username: Optional[str] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get user info using REST endpoint. User contains only explicitly set attributes in 'attrib'. @@ -784,7 +784,7 @@ def get_user( username (Optional[str]): Username. Returns: - Optional[Dict[str, Any]]: User info or None if user is not + Optional[dict[str, Any]]: User info or None if user is not found. """ @@ -1059,7 +1059,7 @@ def upload_reviewable( content_type: Optional[str] = None, filename: Optional[str] = None, progress: Optional[TransferProgress] = None, - headers: Optional[Dict[str, Any]] = None, + headers: Optional[dict[str, Any]] = None, **kwargs, ) -> requests.Response: """Upload reviewable file to server. @@ -1074,7 +1074,7 @@ def upload_reviewable( filename (Optional[str]): User as original filename. Filename from 'filepath' is used when not filled. progress (Optional[TransferProgress]): Progress. - headers (Optional[Dict[str, Any]]): Headers. + headers (Optional[dict[str, Any]]): Headers. Returns: requests.Response: Server response. @@ -1107,7 +1107,7 @@ def trigger_server_restart(): def query_graphql( query: str, - variables: Optional[Dict[str, Any]] = None, + variables: Optional[dict[str, Any]] = None, ) -> GraphQlResponse: """Execute GraphQl query. @@ -1127,12 +1127,12 @@ def query_graphql( ) -def get_graphql_schema() -> Dict[str, Any]: +def get_graphql_schema() -> dict[str, Any]: con = get_server_api_connection() return con.get_graphql_schema() -def get_server_schema() -> Optional[Dict[str, Any]]: +def get_server_schema() -> Optional[dict[str, Any]]: """Get server schema with info, url paths, components etc. Todos: @@ -1146,7 +1146,7 @@ def get_server_schema() -> Optional[Dict[str, Any]]: return con.get_server_schema() -def get_schemas() -> Dict[str, Any]: +def get_schemas() -> dict[str, Any]: """Get components schema. Name of components does not match entity type names e.g. 'project' is @@ -1179,7 +1179,7 @@ def reset_attributes_schema(): def set_attribute_config( attribute_name: str, data: "AttributeSchemaDataDict", - scope: List["AttributeScope"], + scope: list["AttributeScope"], position: Optional[int] = None, builtin: bool = False, ): @@ -1212,7 +1212,7 @@ def remove_attribute_config( def get_attributes_for_type( entity_type: "AttributeScope", -) -> Dict[str, "AttributeSchemaDict"]: +) -> dict[str, "AttributeSchemaDict"]: """Get attribute schemas available for an entity type. Example:: @@ -1257,7 +1257,7 @@ def get_attributes_for_type( def get_attributes_fields_for_type( entity_type: "AttributeScope", -) -> Set[str]: +) -> set[str]: """Prepare attribute fields for entity type. Returns: @@ -1272,7 +1272,7 @@ def get_attributes_fields_for_type( def get_default_fields_for_type( entity_type: str, -) -> Set[str]: +) -> set[str]: """Default fields for entity type. Returns most of commonly used fields from server. @@ -1319,12 +1319,12 @@ def create_installer( version: str, python_version: str, platform_name: str, - python_modules: Dict[str, str], - runtime_python_modules: Dict[str, str], + python_modules: dict[str, str], + runtime_python_modules: dict[str, str], checksum: str, checksum_algorithm: str, file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, + sources: Optional[list[dict[str, Any]]] = None, ): """Create new installer information on server. @@ -1368,7 +1368,7 @@ def create_installer( def update_installer( filename: str, - sources: List[Dict[str, Any]], + sources: list[dict[str, Any]], ): """Update installer information on server. @@ -1484,13 +1484,13 @@ def get_dependency_packages() -> "DependencyPackagesDict": def create_dependency_package( filename: str, - python_modules: Dict[str, str], - source_addons: Dict[str, str], + python_modules: dict[str, str], + source_addons: dict[str, str], installer_version: str, checksum: str, checksum_algorithm: str, file_size: int, - sources: Optional[List[Dict[str, Any]]] = None, + sources: Optional[list[dict[str, Any]]] = None, platform_name: Optional[str] = None, ): """Create dependency package on server. @@ -1538,7 +1538,7 @@ def create_dependency_package( def update_dependency_package( filename: str, - sources: List[Dict[str, Any]], + sources: list[dict[str, Any]], ): """Update dependency package metadata on server. @@ -1676,14 +1676,14 @@ def get_bundles() -> "BundlesInfoDict": def create_bundle( name: str, - addon_versions: Dict[str, str], + addon_versions: dict[str, str], installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[Dict[str, "DevBundleAddonInfoDict"]] = None, + dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, ): """Create bundle on server. @@ -1739,14 +1739,14 @@ def create_bundle( def update_bundle( bundle_name: str, - addon_versions: Optional[Dict[str, str]] = None, + addon_versions: Optional[dict[str, str]] = None, installer_version: Optional[str] = None, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[Dict[str, "DevBundleAddonInfoDict"]] = None, + dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, ): """Update bundle on server. @@ -1788,15 +1788,15 @@ def update_bundle( def check_bundle_compatibility( name: str, - addon_versions: Dict[str, str], + addon_versions: dict[str, str], installer_version: str, - dependency_packages: Optional[Dict[str, str]] = None, + dependency_packages: Optional[dict[str, str]] = None, is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[Dict[str, "DevBundleAddonInfoDict"]] = None, -) -> Dict[str, Any]: + dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, +) -> dict[str, Any]: """Check bundle compatibility. Can be used as per-flight validation before creating bundle. @@ -1818,7 +1818,7 @@ def check_bundle_compatibility( dev addons. Can be used only if 'is_dev' is set to 'True'. Returns: - Dict[str, Any]: Server response, with 'success' and 'issues'. + dict[str, Any]: Server response, with 'success' and 'issues'. """ con = get_server_api_connection() @@ -1850,7 +1850,7 @@ def delete_bundle( ) -def get_project_anatomy_presets() -> List["AnatomyPresetDict"]: +def get_project_anatomy_presets() -> list["AnatomyPresetDict"]: """Anatomy presets available on server. Content has basic information about presets. Example output:: @@ -1929,7 +1929,7 @@ def get_build_in_anatomy_preset() -> "AnatomyPresetDict": def get_project_root_overrides( project_name: str, -) -> Dict[str, Dict[str, str]]: +) -> dict[str, dict[str, str]]: """Root overrides per site name. Method is based on logged user and can't be received for any other @@ -1952,7 +1952,7 @@ def get_project_root_overrides( def get_project_roots_by_site( project_name: str, -) -> Dict[str, Dict[str, str]]: +) -> dict[str, dict[str, str]]: """Root overrides per site name. Method is based on logged user and can't be received for any other @@ -1980,7 +1980,7 @@ def get_project_roots_by_site( def get_project_root_overrides_by_site_id( project_name: str, site_id: Optional[str] = None, -) -> Dict[str, str]: +) -> dict[str, str]: """Root overrides for site. If site id is not passed a site set in current api object is used @@ -2006,7 +2006,7 @@ def get_project_root_overrides_by_site_id( def get_project_roots_for_site( project_name: str, site_id: Optional[str] = None, -) -> Dict[str, str]: +) -> dict[str, str]: """Root overrides for site. If site id is not passed a site set in current api object is used @@ -2035,7 +2035,7 @@ def get_project_roots_for_site( def get_project_roots_by_site_id( project_name: str, site_id: Optional[str] = None, -) -> Dict[str, str]: +) -> dict[str, str]: """Root values for a site. If site id is not passed a site set in current api object is used @@ -2061,7 +2061,7 @@ def get_project_roots_by_site_id( def get_project_roots_by_platform( project_name: str, platform_name: Optional[str] = None, -) -> Dict[str, str]: +) -> dict[str, str]: """Root values for a site. If platform name is not passed current platform name is used instead. @@ -2090,7 +2090,7 @@ def get_addon_settings_schema( addon_name: str, addon_version: str, project_name: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Sudio/Project settings schema of an addon. Project schema may look differently as some enums are based on project @@ -2117,7 +2117,7 @@ def get_addon_settings_schema( def get_addon_site_settings_schema( addon_name: str, addon_version: str, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Site settings schema of an addon. Args: @@ -2139,7 +2139,7 @@ def get_addon_studio_settings( addon_name: str, addon_version: str, variant: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Addon studio settings. Receive studio settings for specific version of an addon. @@ -2169,7 +2169,7 @@ def get_addon_project_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Addon project settings. Receive project settings for specific version of an addon. The settings @@ -2214,7 +2214,7 @@ def get_addon_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Receive addon settings. Receive addon settings based on project name value. Some arguments may @@ -2254,7 +2254,7 @@ def get_addon_site_settings( addon_name: str, addon_version: str, site_id: Optional[str] = None, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Site settings of an addon. If site id is not available an empty dictionary is returned. @@ -2283,7 +2283,7 @@ def get_bundle_settings( variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Get complete set of settings for given data. If project is not passed then studio settings are returned. If variant @@ -2331,7 +2331,7 @@ def get_addons_studio_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """All addons settings in one bulk. Warnings: @@ -2373,7 +2373,7 @@ def get_addons_project_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Project settings of all addons. Server returns information about used addon versions, so full output @@ -2433,7 +2433,7 @@ def get_addons_settings( site_id: Optional[str] = None, use_site: bool = True, only_values: bool = True, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Universal function to receive all addon settings. Based on 'project_name' will receive studio settings or project @@ -2471,7 +2471,7 @@ def get_addons_settings( ) -def get_secrets() -> List["SecretDict"]: +def get_secrets() -> list["SecretDict"]: """Get all secrets. Example output:: @@ -2582,10 +2582,10 @@ def get_rest_entity_by_id( def send_batch_operations( project_name: str, - operations: List[Dict[str, Any]], + operations: list[dict[str, Any]], can_fail: bool = False, raise_on_fail: bool = True, -) -> List[Dict[str, Any]]: +) -> list[dict[str, Any]]: """Post multiple CRUD operations to server. When multiple changes should be made on server side this is the best From 815eccca9ad77bcc6609971102371227133ebd81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:21:42 +0200 Subject: [PATCH 142/506] added get_site_id to base --- ayon_api/_base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ayon_api/_base.py b/ayon_api/_base.py index 95021aec7..29e193e2d 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_base.py @@ -63,6 +63,9 @@ def raw_delete(self, entrypoint: str, **kwargs): def get_default_settings_variant(self) -> str: raise NotImplementedError() + def get_site_id(self) -> Optional[str]: + raise NotImplementedError() + def get_default_fields_for_type(self, entity_type: str) -> set[str]: raise NotImplementedError() From 200574f7e04f22f431d4d9560689076ecd5f7eef Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:22:05 +0200 Subject: [PATCH 143/506] removed unused imports --- ayon_api/server_api.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 32cc9e727..b2a8c17be 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -100,13 +100,7 @@ SecretDict, AnyEntityDict, - TaskDict, - ProductDict, - VersionDict, - RepresentationDict, - WorkfileInfoDict, - ProductTypeDict, StreamType, ) From cf0e691394faa72031e1cd11c2e2075da06441df Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:22:22 +0200 Subject: [PATCH 144/506] import placeholder from base --- automated_api.py | 2 +- ayon_api/server_api.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/automated_api.py b/automated_api.py index e9804adfd..41754e412 100644 --- a/automated_api.py +++ b/automated_api.py @@ -235,7 +235,7 @@ def _add_typehint(param_name, param, api_globals): def _kw_default_to_str(param_name, param, api_globals): - from ayon_api.server_api import _PLACEHOLDER + from ayon_api._base import _PLACEHOLDER from ayon_api.utils import NOT_SET if param.default is inspect.Parameter.empty: diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b2a8c17be..736630cd6 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -67,7 +67,6 @@ get_machine_name, fill_own_attribs, ) -from ._base import _PLACEHOLDER from ._actions import _ActionsAPI from ._activities import _ActivitiesAPI from ._addons import _AddonsAPI From 013cc8f59f6e2201052561c2419c87cb4f6a4ecf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:23:16 +0200 Subject: [PATCH 145/506] move all bundles and addons endpoints to single class --- ayon_api/_addons.py | 672 ++++++++++++++++++++++++++++++++++++++++- ayon_api/server_api.py | 663 ---------------------------------------- 2 files changed, 669 insertions(+), 666 deletions(-) diff --git a/ayon_api/_addons.py b/ayon_api/_addons.py index 10337ec50..e09bf2055 100644 --- a/ayon_api/_addons.py +++ b/ayon_api/_addons.py @@ -1,6 +1,6 @@ import os import typing -from typing import Optional +from typing import Optional, Any from .utils import ( RequestTypes, @@ -10,10 +10,246 @@ from ._base import _BaseServerAPI if typing.TYPE_CHECKING: - from .typing import AddonsInfoDict + from .typing import ( + AddonsInfoDict, + BundlesInfoDict, + DevBundleAddonInfoDict, + ) class _AddonsAPI(_BaseServerAPI): + def get_bundles(self) -> "BundlesInfoDict": + """Server bundles with basic information. + + This is example output:: + + { + "bundles": [ + { + "name": "my_bundle", + "createdAt": "2023-06-12T15:37:02.420260", + "installerVersion": "1.0.0", + "addons": { + "core": "1.2.3" + }, + "dependencyPackages": { + "windows": "a_windows_package123.zip", + "linux": "a_linux_package123.zip", + "darwin": "a_mac_package123.zip" + }, + "isProduction": False, + "isStaging": False + } + ], + "productionBundle": "my_bundle", + "stagingBundle": "test_bundle" + } + + Returns: + dict[str, Any]: Server bundles with basic information. + + """ + response = self.get("bundles") + response.raise_for_status() + return response.data + + def create_bundle( + self, + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[ + dict[str, "DevBundleAddonInfoDict"]] = None, + ): + """Create bundle on server. + + Bundle cannot be changed once is created. Only isProduction, isStaging + and dependency packages can change after creation. In case dev bundle + is created, it is possible to change anything, but it is not possible + to mark bundle as dev and production or staging at the same time. + + Development addon config can define custom path to client code. It is + used only for dev bundles. + + Example of 'dev_addons_config':: + + ```json + { + "core": { + "enabled": true, + "path": "/path/to/ayon-core/client" + } + } + ``` + + Args: + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. + + """ + body = { + "name": name, + "installerVersion": installer_version, + "addons": addon_versions, + } + + for key, value in ( + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ("isDev", is_dev), + ("activeUser", dev_active_user), + ("addonDevelopment", dev_addons_config), + ): + if value is not None: + body[key] = value + + response = self.post("bundles", **body) + response.raise_for_status() + + def update_bundle( + self, + bundle_name: str, + addon_versions: Optional[dict[str, str]] = None, + installer_version: Optional[str] = None, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[ + dict[str, "DevBundleAddonInfoDict"]] = None, + ): + """Update bundle on server. + + Dependency packages can be update only for single platform. Others + will be left untouched. Use 'None' value to unset dependency package + from bundle. + + Args: + bundle_name (str): Name of bundle. + addon_versions (Optional[dict[str, str]]): Addon versions, + possible only for dev bundles. + installer_version (Optional[str]): Installer version, possible + only for dev bundles. + dependency_packages (Optional[dict[str, str]]): Dependency pacakge + names that should be used with the bundle. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only for dev bundles. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only for dev bundles. + + """ + body = { + key: value + for key, value in ( + ("installerVersion", installer_version), + ("addons", addon_versions), + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ("isDev", is_dev), + ("activeUser", dev_active_user), + ("addonDevelopment", dev_addons_config), + ) + if value is not None + } + + response = self.patch( + f"bundles/{bundle_name}", + **body + ) + response.raise_for_status() + + def check_bundle_compatibility( + self, + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[ + dict[str, "DevBundleAddonInfoDict"]] = None, + ) -> dict[str, Any]: + """Check bundle compatibility. + + Can be used as per-flight validation before creating bundle. + + Args: + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. + + Returns: + dict[str, Any]: Server response, with 'success' and 'issues'. + + """ + body = { + "name": name, + "installerVersion": installer_version, + "addons": addon_versions, + } + + for key, value in ( + ("dependencyPackages", dependency_packages), + ("isProduction", is_production), + ("isStaging", is_staging), + ("isDev", is_dev), + ("activeUser", dev_active_user), + ("addonDevelopment", dev_addons_config), + ): + if value is not None: + body[key] = value + + response = self.post("bundles/check", **body) + response.raise_for_status() + return response.data + + def delete_bundle(self, bundle_name: str): + """Delete bundle from server. + + Args: + bundle_name (str): Name of bundle to delete. + + """ + response = self.delete(f"bundles/{bundle_name}") + response.raise_for_status() + def get_addon_endpoint( self, addon_name: str, @@ -69,7 +305,7 @@ def get_addon_url( """Calculate url to addon route. Examples: - + >>> from ayon_api import ServerAPI >>> api = ServerAPI("https://your.url.com") >>> api.get_addon_url( ... "example", "1.0.0", "private", "my.zip") @@ -215,3 +451,433 @@ def download_addon_private_file( url, dst_filepath, chunk_size=chunk_size, progress=progress ) return dst_filepath + + + def get_addon_settings_schema( + self, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None + ) -> dict[str, Any]: + """Sudio/Project settings schema of an addon. + + Project schema may look differently as some enums are based on project + values. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Optional[str]): Schema for specific project or + default studio schemas. + + Returns: + dict[str, Any]: Schema of studio/project settings. + + """ + args = tuple() + if project_name: + args = (project_name, ) + + endpoint = self.get_addon_endpoint( + addon_name, addon_version, "schema", *args + ) + result = self.get(endpoint) + result.raise_for_status() + return result.data + + def get_addon_site_settings_schema( + self, addon_name: str, addon_version: str + ) -> dict[str, Any]: + """Site settings schema of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + + Returns: + dict[str, Any]: Schema of site settings. + + """ + result = self.get( + f"addons/{addon_name}/{addon_version}/siteSettings/schema" + ) + result.raise_for_status() + return result.data + + def get_addon_studio_settings( + self, + addon_name: str, + addon_version: str, + variant: Optional[str] = None, + ) -> dict[str, Any]: + """Addon studio settings. + + Receive studio settings for specific version of an addon. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + + Returns: + dict[str, Any]: Addon settings. + + """ + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({"variant": variant or None}) + + result = self.get( + f"addons/{addon_name}/{addon_version}/settings{query}" + ) + result.raise_for_status() + return result.data + + def get_addon_project_settings( + self, + addon_name: str, + addon_version: str, + project_name: str, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True + ) -> dict[str, Any]: + """Addon project settings. + + Receive project settings for specific version of an addon. The settings + may be with site overrides when enabled. + + Site id is filled with current connection site id if not passed. To + make sure any site id is used set 'use_site' to 'False'. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Name of project for which the settings are + received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. + + Returns: + dict[str, Any]: Addon settings. + + """ + if not use_site: + site_id = None + elif not site_id: + site_id = self.get_site_id() + + if variant is None: + variant = self.get_default_settings_variant() + + query = prepare_query_string({ + "site": site_id or None, + "variant": variant or None, + }) + result = self.get( + f"addons/{addon_name}/{addon_version}" + f"/settings/{project_name}{query}" + ) + result.raise_for_status() + return result.data + + def get_addon_settings( + self, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True + ) -> dict[str, Any]: + """Receive addon settings. + + Receive addon settings based on project name value. Some arguments may + be ignored if 'project_name' is set to 'None'. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Optional[str]): Name of project for which the + settings are received. A studio settings values are received + if is 'None'. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using + site overrides set to 'False'. In that case won't be applied + any site overrides. + + Returns: + dict[str, Any]: Addon settings. + + """ + if project_name is None: + return self.get_addon_studio_settings( + addon_name, addon_version, variant + ) + return self.get_addon_project_settings( + addon_name, addon_version, project_name, variant, site_id, use_site + ) + + def get_addon_site_settings( + self, + addon_name: str, + addon_version: str, + site_id: Optional[str] = None, + ) -> dict[str, Any]: + """Site settings of an addon. + + If site id is not available an empty dictionary is returned. + + Args: + addon_name (str): Name of addon. + addon_version (str): Version of addon. + site_id (Optional[str]): Name of site for which should be settings + returned. using 'site_id' attribute if not passed. + + Returns: + dict[str, Any]: Site settings. + + """ + if site_id is None: + site_id = self.get_site_id() + + if not site_id: + return {} + + query = prepare_query_string({"site": site_id}) + result = self.get( + f"addons/{addon_name}/{addon_version}/siteSettings{query}" + ) + result.raise_for_status() + return result.data + + def get_bundle_settings( + self, + bundle_name: Optional[str] = None, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + ) -> dict[str, Any]: + """Get complete set of settings for given data. + + If project is not passed then studio settings are returned. If variant + is not passed 'default_settings_variant' is used. If bundle name is + not passed then current production/staging bundle is used, based on + variant value. + + Output contains addon settings and site settings in single dictionary. + + Todos: + - test how it behaves if there is not any bundle. + - test how it behaves if there is not any production/staging + bundle. + + Example output:: + + { + "addons": [ + { + "name": "addon-name", + "version": "addon-version", + "settings": {...}, + "siteSettings": {...} + } + ] + } + + Returns: + dict[str, Any]: All settings for single bundle. + + """ + if not use_site: + site_id = None + elif not site_id: + site_id = self.get_site_id() + + query = prepare_query_string({ + "project_name": project_name or None, + "bundle_name": bundle_name or None, + "variant": variant or self.get_default_settings_variant() or None, + "site_id": site_id, + }) + response = self.get(f"settings{query}") + response.raise_for_status() + return response.data + + def get_addons_studio_settings( + self, + bundle_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, + ) -> dict[str, Any]: + """All addons settings in one bulk. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. + + Returns: + dict[str, Any]: Settings of all addons on server. + + """ + output = self.get_bundle_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site + ) + if only_values: + output = { + addon["name"]: addon["settings"] + for addon in output["addons"] + } + return output + + def get_addons_project_settings( + self, + project_name: str, + bundle_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, + ) -> dict[str, Any]: + """Project settings of all addons. + + Server returns information about used addon versions, so full output + looks like: + + ```json + { + "settings": {...}, + "addons": {...} + } + ``` + + The output can be limited to only values. To do so is 'only_values' + argument which is by default set to 'True'. In that case output + contains only value of 'settings' key. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + project_name (str): Name of project for which are settings + received. + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. + + Returns: + dict[str, Any]: Settings of all addons on server for passed + project. + + """ + if not project_name: + raise ValueError("Project name must be passed.") + + output = self.get_bundle_settings( + project_name=project_name, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site + ) + if only_values: + output = { + addon["name"]: addon["settings"] + for addon in output["addons"] + } + return output + + def get_addons_settings( + self, + bundle_name: Optional[str] = None, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, + ) -> dict[str, Any]: + """Universal function to receive all addon settings. + + Based on 'project_name' will receive studio settings or project + settings. In case project is not passed is 'site_id' ignored. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + project_name (Optional[str]): Name of project for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. + only_values (Optional[bool]): Only settings values will be + returned. By default, is set to 'True'. + + """ + if project_name is None: + return self.get_addons_studio_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values + ) + + return self.get_addons_project_settings( + project_name=project_name, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values + ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 736630cd6..fae685682 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -93,8 +93,6 @@ AttributesSchemaDict, InstallersInfoDict, DependencyPackagesDict, - DevBundleAddonInfoDict, - BundlesInfoDict, AnatomyPresetDict, SecretDict, @@ -2357,238 +2355,6 @@ def upload_dependency_package( route = self._get_dependency_package_route(dst_filename) self.upload_file(route, src_filepath, progress=progress) - def get_bundles(self) -> "BundlesInfoDict": - """Server bundles with basic information. - - This is example output:: - - { - "bundles": [ - { - "name": "my_bundle", - "createdAt": "2023-06-12T15:37:02.420260", - "installerVersion": "1.0.0", - "addons": { - "core": "1.2.3" - }, - "dependencyPackages": { - "windows": "a_windows_package123.zip", - "linux": "a_linux_package123.zip", - "darwin": "a_mac_package123.zip" - }, - "isProduction": False, - "isStaging": False - } - ], - "productionBundle": "my_bundle", - "stagingBundle": "test_bundle" - } - - Returns: - dict[str, Any]: Server bundles with basic information. - - """ - response = self.get("bundles") - response.raise_for_status() - return response.data - - def create_bundle( - self, - name: str, - addon_versions: dict[str, str], - installer_version: str, - dependency_packages: Optional[dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - dict[str, "DevBundleAddonInfoDict"]] = None, - ): - """Create bundle on server. - - Bundle cannot be changed once is created. Only isProduction, isStaging - and dependency packages can change after creation. In case dev bundle - is created, it is possible to change anything, but it is not possible - to mark bundle as dev and production or staging at the same time. - - Development addon config can define custom path to client code. It is - used only for dev bundles. - - Example of 'dev_addons_config':: - - ```json - { - "core": { - "enabled": true, - "path": "/path/to/ayon-core/client" - } - } - ``` - - Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. - - """ - body = { - "name": name, - "installerVersion": installer_version, - "addons": addon_versions, - } - - for key, value in ( - ("dependencyPackages", dependency_packages), - ("isProduction", is_production), - ("isStaging", is_staging), - ("isDev", is_dev), - ("activeUser", dev_active_user), - ("addonDevelopment", dev_addons_config), - ): - if value is not None: - body[key] = value - - response = self.post("bundles", **body) - response.raise_for_status() - - def update_bundle( - self, - bundle_name: str, - addon_versions: Optional[dict[str, str]] = None, - installer_version: Optional[str] = None, - dependency_packages: Optional[dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - dict[str, "DevBundleAddonInfoDict"]] = None, - ): - """Update bundle on server. - - Dependency packages can be update only for single platform. Others - will be left untouched. Use 'None' value to unset dependency package - from bundle. - - Args: - bundle_name (str): Name of bundle. - addon_versions (Optional[dict[str, str]]): Addon versions, - possible only for dev bundles. - installer_version (Optional[str]): Installer version, possible - only for dev bundles. - dependency_packages (Optional[dict[str, str]]): Dependency pacakge - names that should be used with the bundle. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only for dev bundles. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only for dev bundles. - - """ - body = { - key: value - for key, value in ( - ("installerVersion", installer_version), - ("addons", addon_versions), - ("dependencyPackages", dependency_packages), - ("isProduction", is_production), - ("isStaging", is_staging), - ("isDev", is_dev), - ("activeUser", dev_active_user), - ("addonDevelopment", dev_addons_config), - ) - if value is not None - } - - response = self.patch( - f"bundles/{bundle_name}", - **body - ) - response.raise_for_status() - - def check_bundle_compatibility( - self, - name: str, - addon_versions: dict[str, str], - installer_version: str, - dependency_packages: Optional[dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - dict[str, "DevBundleAddonInfoDict"]] = None, - ) -> dict[str, Any]: - """Check bundle compatibility. - - Can be used as per-flight validation before creating bundle. - - Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. - - Returns: - dict[str, Any]: Server response, with 'success' and 'issues'. - - """ - body = { - "name": name, - "installerVersion": installer_version, - "addons": addon_versions, - } - - for key, value in ( - ("dependencyPackages", dependency_packages), - ("isProduction", is_production), - ("isStaging", is_staging), - ("isDev", is_dev), - ("activeUser", dev_active_user), - ("addonDevelopment", dev_addons_config), - ): - if value is not None: - body[key] = value - - response = self.post("bundles/check", **body) - response.raise_for_status() - return response.data - - def delete_bundle(self, bundle_name: str): - """Delete bundle from server. - - Args: - bundle_name (str): Name of bundle to delete. - - """ - response = self.delete(f"bundles/{bundle_name}") - response.raise_for_status() - # Anatomy presets def get_project_anatomy_presets(self) -> list["AnatomyPresetDict"]: """Anatomy presets available on server. @@ -2878,435 +2644,6 @@ def get_project_roots_by_platform( project_name, platform_name=platform_name ) - def get_addon_settings_schema( - self, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None - ) -> dict[str, Any]: - """Sudio/Project settings schema of an addon. - - Project schema may look differently as some enums are based on project - values. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (Optional[str]): Schema for specific project or - default studio schemas. - - Returns: - dict[str, Any]: Schema of studio/project settings. - - """ - args = tuple() - if project_name: - args = (project_name, ) - - endpoint = self.get_addon_endpoint( - addon_name, addon_version, "schema", *args - ) - result = self.get(endpoint) - result.raise_for_status() - return result.data - - def get_addon_site_settings_schema( - self, addon_name: str, addon_version: str - ) -> dict[str, Any]: - """Site settings schema of an addon. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - - Returns: - dict[str, Any]: Schema of site settings. - - """ - result = self.get( - f"addons/{addon_name}/{addon_version}/siteSettings/schema" - ) - result.raise_for_status() - return result.data - - def get_addon_studio_settings( - self, - addon_name: str, - addon_version: str, - variant: Optional[str] = None, - ) -> dict[str, Any]: - """Addon studio settings. - - Receive studio settings for specific version of an addon. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - - Returns: - dict[str, Any]: Addon settings. - - """ - if variant is None: - variant = self.default_settings_variant - - query = prepare_query_string({"variant": variant or None}) - - result = self.get( - f"addons/{addon_name}/{addon_version}/settings{query}" - ) - result.raise_for_status() - return result.data - - def get_addon_project_settings( - self, - addon_name: str, - addon_version: str, - project_name: str, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True - ) -> dict[str, Any]: - """Addon project settings. - - Receive project settings for specific version of an addon. The settings - may be with site overrides when enabled. - - Site id is filled with current connection site id if not passed. To - make sure any site id is used set 'use_site' to 'False'. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (str): Name of project for which the settings are - received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Name of site which is used for site - overrides. Is filled with connection 'site_id' attribute - if not passed. - use_site (Optional[bool]): To force disable option of using site - overrides set to 'False'. In that case won't be applied - any site overrides. - - Returns: - dict[str, Any]: Addon settings. - - """ - if not use_site: - site_id = None - elif not site_id: - site_id = self.site_id - - if variant is None: - variant = self.default_settings_variant - - query = prepare_query_string({ - "site": site_id or None, - "variant": variant or None, - }) - result = self.get( - f"addons/{addon_name}/{addon_version}" - f"/settings/{project_name}{query}" - ) - result.raise_for_status() - return result.data - - def get_addon_settings( - self, - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True - ) -> dict[str, Any]: - """Receive addon settings. - - Receive addon settings based on project name value. Some arguments may - be ignored if 'project_name' is set to 'None'. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (Optional[str]): Name of project for which the - settings are received. A studio settings values are received - if is 'None'. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Name of site which is used for site - overrides. Is filled with connection 'site_id' attribute - if not passed. - use_site (Optional[bool]): To force disable option of using - site overrides set to 'False'. In that case won't be applied - any site overrides. - - Returns: - dict[str, Any]: Addon settings. - - """ - if project_name is None: - return self.get_addon_studio_settings( - addon_name, addon_version, variant - ) - return self.get_addon_project_settings( - addon_name, addon_version, project_name, variant, site_id, use_site - ) - - def get_addon_site_settings( - self, - addon_name: str, - addon_version: str, - site_id: Optional[str] = None, - ) -> dict[str, Any]: - """Site settings of an addon. - - If site id is not available an empty dictionary is returned. - - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - site_id (Optional[str]): Name of site for which should be settings - returned. using 'site_id' attribute if not passed. - - Returns: - dict[str, Any]: Site settings. - - """ - if site_id is None: - site_id = self.site_id - - if not site_id: - return {} - - query = prepare_query_string({"site": site_id}) - result = self.get( - f"addons/{addon_name}/{addon_version}/siteSettings{query}" - ) - result.raise_for_status() - return result.data - - def get_bundle_settings( - self, - bundle_name: Optional[str] = None, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - ) -> dict[str, Any]: - """Get complete set of settings for given data. - - If project is not passed then studio settings are returned. If variant - is not passed 'default_settings_variant' is used. If bundle name is - not passed then current production/staging bundle is used, based on - variant value. - - Output contains addon settings and site settings in single dictionary. - - Todos: - - test how it behaves if there is not any bundle. - - test how it behaves if there is not any production/staging - bundle. - - Example output:: - - { - "addons": [ - { - "name": "addon-name", - "version": "addon-version", - "settings": {...}, - "siteSettings": {...} - } - ] - } - - Returns: - dict[str, Any]: All settings for single bundle. - - """ - if not use_site: - site_id = None - elif not site_id: - site_id = self.site_id - - query = prepare_query_string({ - "project_name": project_name or None, - "bundle_name": bundle_name or None, - "variant": variant or self.default_settings_variant or None, - "site_id": site_id, - }) - response = self.get(f"settings{query}") - response.raise_for_status() - return response.data - - def get_addons_studio_settings( - self, - bundle_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, - ) -> dict[str, Any]: - """All addons settings in one bulk. - - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. - - Args: - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Site id for which want to receive - site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. - only_values (Optional[bool]): Output will contain only settings - values without metadata about addons. - - Returns: - dict[str, Any]: Settings of all addons on server. - - """ - output = self.get_bundle_settings( - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site - ) - if only_values: - output = { - addon["name"]: addon["settings"] - for addon in output["addons"] - } - return output - - def get_addons_project_settings( - self, - project_name: str, - bundle_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, - ) -> dict[str, Any]: - """Project settings of all addons. - - Server returns information about used addon versions, so full output - looks like: - - ```json - { - "settings": {...}, - "addons": {...} - } - ``` - - The output can be limited to only values. To do so is 'only_values' - argument which is by default set to 'True'. In that case output - contains only value of 'settings' key. - - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. - - Args: - project_name (str): Name of project for which are settings - received. - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Site id for which want to receive - site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. - only_values (Optional[bool]): Output will contain only settings - values without metadata about addons. - - Returns: - dict[str, Any]: Settings of all addons on server for passed - project. - - """ - if not project_name: - raise ValueError("Project name must be passed.") - - output = self.get_bundle_settings( - project_name=project_name, - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site - ) - if only_values: - output = { - addon["name"]: addon["settings"] - for addon in output["addons"] - } - return output - - def get_addons_settings( - self, - bundle_name: Optional[str] = None, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, - ) -> dict[str, Any]: - """Universal function to receive all addon settings. - - Based on 'project_name' will receive studio settings or project - settings. In case project is not passed is 'site_id' ignored. - - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. - - Args: - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - project_name (Optional[str]): Name of project for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Id of site for which want to receive - site overrides. - use_site (Optional[bool]): To force disable option of using site - overrides set to 'False'. In that case won't be applied - any site overrides. - only_values (Optional[bool]): Only settings values will be - returned. By default, is set to 'True'. - - """ - if project_name is None: - return self.get_addons_studio_settings( - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values - ) - - return self.get_addons_project_settings( - project_name=project_name, - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values - ) - def get_secrets(self) -> list["SecretDict"]: """Get all secrets. From de778903c83ec13124cbf30f6a4ec599f8c0c41f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 13 Aug 2025 16:23:32 +0200 Subject: [PATCH 146/506] rename the class --- automated_api.py | 4 ++-- ayon_api/_addons.py | 2 +- ayon_api/server_api.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/automated_api.py b/automated_api.py index 41754e412..3dbec8aa5 100644 --- a/automated_api.py +++ b/automated_api.py @@ -337,7 +337,7 @@ def prepare_api_functions(api_globals): ServerAPI, _ActionsAPI, _ActivitiesAPI, - _AddonsAPI, + _BundlesAddonsAPI, _EventsAPI, _FoldersAPI, _TasksAPI, @@ -355,7 +355,7 @@ def prepare_api_functions(api_globals): _items = list(ServerAPI.__dict__.items()) _items.extend(_ActionsAPI.__dict__.items()) _items.extend(_ActivitiesAPI.__dict__.items()) - _items.extend(_AddonsAPI.__dict__.items()) + _items.extend(_BundlesAddonsAPI.__dict__.items()) _items.extend(_EventsAPI.__dict__.items()) _items.extend(_LinksAPI.__dict__.items()) _items.extend(_ListsAPI.__dict__.items()) diff --git a/ayon_api/_addons.py b/ayon_api/_addons.py index e09bf2055..1b3dea11b 100644 --- a/ayon_api/_addons.py +++ b/ayon_api/_addons.py @@ -17,7 +17,7 @@ ) -class _AddonsAPI(_BaseServerAPI): +class _BundlesAddonsAPI(_BaseServerAPI): def get_bundles(self) -> "BundlesInfoDict": """Server bundles with basic information. diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index fae685682..3c70302fe 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -69,7 +69,7 @@ ) from ._actions import _ActionsAPI from ._activities import _ActivitiesAPI -from ._addons import _AddonsAPI +from ._addons import _BundlesAddonsAPI from ._events import _EventsAPI from ._links import _LinksAPI from ._lists import _ListsAPI @@ -216,7 +216,7 @@ def as_user(self, username): class ServerAPI( _ActionsAPI, _ActivitiesAPI, - _AddonsAPI, + _BundlesAddonsAPI, _EventsAPI, _ProjectsAPI, _FoldersAPI, From 6ba7388cad816eb8fe8df933d7a24fca861d264c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 10:52:18 +0200 Subject: [PATCH 147/506] rename file --- ayon_api/{_addons.py => _bundles_addons.py} | 0 ayon_api/server_api.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename ayon_api/{_addons.py => _bundles_addons.py} (100%) diff --git a/ayon_api/_addons.py b/ayon_api/_bundles_addons.py similarity index 100% rename from ayon_api/_addons.py rename to ayon_api/_bundles_addons.py diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 3c70302fe..448ba1150 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -69,7 +69,7 @@ ) from ._actions import _ActionsAPI from ._activities import _ActivitiesAPI -from ._addons import _BundlesAddonsAPI +from ._bundles_addons import _BundlesAddonsAPI from ._events import _EventsAPI from ._links import _LinksAPI from ._lists import _ListsAPI From 74c65d3a464ff002604907d1045804b05a7c19d9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:04:05 +0200 Subject: [PATCH 148/506] move apis to subfolder --- automated_api.py | 58 +++++++++---------- ayon_api/_api_helpers/__init__.py | 34 +++++++++++ .../{_actions.py => _api_helpers/actions.py} | 8 +-- .../activities.py} | 11 ++-- ayon_api/{_base.py => _api_helpers/base.py} | 6 +- .../bundles_addons.py} | 9 +-- .../{_events.py => _api_helpers/events.py} | 12 ++-- .../{_folders.py => _api_helpers/folders.py} | 13 +++-- ayon_api/{_links.py => _api_helpers/links.py} | 9 +-- ayon_api/{_lists.py => _api_helpers/lists.py} | 11 ++-- .../products.py} | 11 ++-- .../projects.py} | 13 +++-- .../representations.py} | 13 +++-- ayon_api/{_tasks.py => _api_helpers/tasks.py} | 11 ++-- .../thumbnails.py} | 7 ++- .../versions.py} | 13 +++-- .../workfiles.py} | 9 +-- 17 files changed, 148 insertions(+), 100 deletions(-) create mode 100644 ayon_api/_api_helpers/__init__.py rename ayon_api/{_actions.py => _api_helpers/actions.py} (98%) rename ayon_api/{_activities.py => _api_helpers/activities.py} (98%) rename ayon_api/{_base.py => _api_helpers/base.py} (96%) rename ayon_api/{_bundles_addons.py => _api_helpers/bundles_addons.py} (99%) rename ayon_api/{_events.py => _api_helpers/events.py} (98%) rename ayon_api/{_folders.py => _api_helpers/folders.py} (98%) rename ayon_api/{_links.py => _api_helpers/links.py} (99%) rename ayon_api/{_lists.py => _api_helpers/lists.py} (98%) rename ayon_api/{_products.py => _api_helpers/products.py} (98%) rename ayon_api/{_projects.py => _api_helpers/projects.py} (98%) rename ayon_api/{_representations.py => _api_helpers/representations.py} (99%) rename ayon_api/{_tasks.py => _api_helpers/tasks.py} (99%) rename ayon_api/{_thumbnails.py => _api_helpers/thumbnails.py} (99%) rename ayon_api/{_versions.py => _api_helpers/versions.py} (98%) rename ayon_api/{_workfiles.py => _api_helpers/workfiles.py} (96%) diff --git a/automated_api.py b/automated_api.py index 3dbec8aa5..d6a24b95f 100644 --- a/automated_api.py +++ b/automated_api.py @@ -235,7 +235,7 @@ def _add_typehint(param_name, param, api_globals): def _kw_default_to_str(param_name, param, api_globals): - from ayon_api._base import _PLACEHOLDER + from ayon_api._api_helpers.base import _PLACEHOLDER from ayon_api.utils import NOT_SET if param.default is inspect.Parameter.empty: @@ -335,38 +335,38 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): def prepare_api_functions(api_globals): from ayon_api.server_api import ( # noqa: E402 ServerAPI, - _ActionsAPI, - _ActivitiesAPI, - _BundlesAddonsAPI, - _EventsAPI, - _FoldersAPI, - _TasksAPI, - _ProductsAPI, - _VersionsAPI, - _LinksAPI, - _ListsAPI, - _ProjectsAPI, - _ThumbnailsAPI, - _WorkfilesAPI, - _RepresentationsAPI, + ActionsAPI, + ActivitiesAPI, + BundlesAddonsAPI, + EventsAPI, + FoldersAPI, + TasksAPI, + ProductsAPI, + VersionsAPI, + LinksAPI, + ListsAPI, + ProjectsAPI, + ThumbnailsAPI, + WorkfilesAPI, + RepresentationsAPI, ) functions = [] _items = list(ServerAPI.__dict__.items()) - _items.extend(_ActionsAPI.__dict__.items()) - _items.extend(_ActivitiesAPI.__dict__.items()) - _items.extend(_BundlesAddonsAPI.__dict__.items()) - _items.extend(_EventsAPI.__dict__.items()) - _items.extend(_LinksAPI.__dict__.items()) - _items.extend(_ListsAPI.__dict__.items()) - _items.extend(_ProjectsAPI.__dict__.items()) - _items.extend(_FoldersAPI.__dict__.items()) - _items.extend(_TasksAPI.__dict__.items()) - _items.extend(_ProductsAPI.__dict__.items()) - _items.extend(_VersionsAPI.__dict__.items()) - _items.extend(_ThumbnailsAPI.__dict__.items()) - _items.extend(_WorkfilesAPI.__dict__.items()) - _items.extend(_RepresentationsAPI.__dict__.items()) + _items.extend(ActionsAPI.__dict__.items()) + _items.extend(ActivitiesAPI.__dict__.items()) + _items.extend(BundlesAddonsAPI.__dict__.items()) + _items.extend(EventsAPI.__dict__.items()) + _items.extend(LinksAPI.__dict__.items()) + _items.extend(ListsAPI.__dict__.items()) + _items.extend(ProjectsAPI.__dict__.items()) + _items.extend(FoldersAPI.__dict__.items()) + _items.extend(TasksAPI.__dict__.items()) + _items.extend(ProductsAPI.__dict__.items()) + _items.extend(VersionsAPI.__dict__.items()) + _items.extend(ThumbnailsAPI.__dict__.items()) + _items.extend(WorkfilesAPI.__dict__.items()) + _items.extend(RepresentationsAPI.__dict__.items()) processed = set() for attr_name, attr in _items: diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py new file mode 100644 index 000000000..538a416d6 --- /dev/null +++ b/ayon_api/_api_helpers/__init__.py @@ -0,0 +1,34 @@ +from .base import BaseServerAPI +from .actions import ActionsAPI +from .activities import ActivitiesAPI +from .bundles_addons import BundlesAddonsAPI +from .events import EventsAPI +from .folders import FoldersAPI +from .links import LinksAPI +from .lists import ListsAPI +from .products import ProductsAPI +from .projects import ProjectsAPI +from .representations import RepresentationsAPI +from .tasks import TasksAPI +from .thumbnails import ThumbnailsAPI +from .versions import VersionsAPI +from .workfiles import WorkfilesAPI + + +__all__ = ( + "BaseServerAPI", + "ActionsAPI", + "ActivitiesAPI", + "BundlesAddonsAPI", + "EventsAPI", + "FoldersAPI", + "LinksAPI", + "ListsAPI", + "ProductsAPI", + "ProjectsAPI", + "RepresentationsAPI", + "TasksAPI", + "ThumbnailsAPI", + "VersionsAPI", + "WorkfilesAPI", +) diff --git a/ayon_api/_actions.py b/ayon_api/_api_helpers/actions.py similarity index 98% rename from ayon_api/_actions.py rename to ayon_api/_api_helpers/actions.py index b575189dd..fcac238c4 100644 --- a/ayon_api/_actions.py +++ b/ayon_api/_api_helpers/actions.py @@ -1,11 +1,11 @@ import typing from typing import Optional, Dict, List, Any -from .utils import prepare_query_string -from ._base import _BaseServerAPI +from ayon_api.utils import prepare_query_string +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( ActionEntityTypes, ActionManifestDict, ActionTriggerResponse, @@ -15,7 +15,7 @@ ) -class _ActionsAPI(_BaseServerAPI): +class ActionsAPI(BaseServerAPI): """Implementation of actions API for ServerAPI.""" def get_actions( self, diff --git a/ayon_api/_activities.py b/ayon_api/_api_helpers/activities.py similarity index 98% rename from ayon_api/_activities.py rename to ayon_api/_api_helpers/activities.py index f9d7796eb..18ed4f411 100644 --- a/ayon_api/_activities.py +++ b/ayon_api/_api_helpers/activities.py @@ -2,21 +2,22 @@ import typing from typing import Optional, Iterable, Generator, Any -from ._base import _BaseServerAPI -from .utils import ( +from ayon_api.utils import ( SortOrder, prepare_list_filters, ) -from .graphql_queries import activities_graphql_query +from ayon_api.graphql_queries import activities_graphql_query + +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( ActivityType, ActivityReferenceType, ) -class _ActivitiesAPI(_BaseServerAPI): +class ActivitiesAPI(BaseServerAPI): def get_activities( self, project_name: str, diff --git a/ayon_api/_base.py b/ayon_api/_api_helpers/base.py similarity index 96% rename from ayon_api/_base.py rename to ayon_api/_api_helpers/base.py index 29e193e2d..f785080ab 100644 --- a/ayon_api/_base.py +++ b/ayon_api/_api_helpers/base.py @@ -5,10 +5,10 @@ import requests -from .utils import TransferProgress, RequestType +from ayon_api.utils import TransferProgress, RequestType if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( AnyEntityDict, ServerVersion, ProjectDict, @@ -17,7 +17,7 @@ _PLACEHOLDER = object() -class _BaseServerAPI: +class BaseServerAPI: def get_server_version(self) -> str: raise NotImplementedError() diff --git a/ayon_api/_bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py similarity index 99% rename from ayon_api/_bundles_addons.py rename to ayon_api/_api_helpers/bundles_addons.py index 1b3dea11b..1b465f0bc 100644 --- a/ayon_api/_bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -2,22 +2,23 @@ import typing from typing import Optional, Any -from .utils import ( +from ayon_api.utils import ( RequestTypes, prepare_query_string, TransferProgress, ) -from ._base import _BaseServerAPI + +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( AddonsInfoDict, BundlesInfoDict, DevBundleAddonInfoDict, ) -class _BundlesAddonsAPI(_BaseServerAPI): +class BundlesAddonsAPI(BaseServerAPI): def get_bundles(self) -> "BundlesInfoDict": """Server bundles with basic information. diff --git a/ayon_api/_events.py b/ayon_api/_api_helpers/events.py similarity index 98% rename from ayon_api/_events.py rename to ayon_api/_api_helpers/events.py index d91aebb07..1c4e0e1cd 100644 --- a/ayon_api/_events.py +++ b/ayon_api/_api_helpers/events.py @@ -2,16 +2,18 @@ import typing from typing import Optional, Any, Iterable, Generator -from ._base import _BaseServerAPI -from .utils import SortOrder, prepare_list_filters -from .graphql_queries import events_graphql_query +from ayon_api.utils import SortOrder, prepare_list_filters +from ayon_api.graphql_queries import events_graphql_query + +from .base import BaseServerAPI if typing.TYPE_CHECKING: from typing import Union - from .typing import EventFilter + + from ayon_api.typing import EventFilter -class _EventsAPI(_BaseServerAPI): +class EventsAPI(BaseServerAPI): def get_event(self, event_id: str) -> Optional[dict[str, Any]]: """Query full event data by id. diff --git a/ayon_api/_folders.py b/ayon_api/_api_helpers/folders.py similarity index 98% rename from ayon_api/_folders.py rename to ayon_api/_api_helpers/folders.py index 76181c8f9..e43e798c3 100644 --- a/ayon_api/_folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -4,26 +4,27 @@ import typing from typing import Optional, Iterable, Generator, Any -from ._base import _BaseServerAPI -from .exceptions import UnsupportedServerVersion -from .utils import ( +from ayon_api.exceptions import UnsupportedServerVersion +from ayon_api.utils import ( prepare_query_string, prepare_list_filters, fill_own_attribs, create_entity_id, NOT_SET, ) -from .graphql_queries import folders_graphql_query +from ayon_api.graphql_queries import folders_graphql_query + +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( FolderDict, FlatFolderDict, ProjectHierarchyDict, ) -class _FoldersAPI(_BaseServerAPI): +class FoldersAPI(BaseServerAPI): def get_rest_folder( self, project_name: str, folder_id: str ) -> Optional["FolderDict"]: diff --git a/ayon_api/_links.py b/ayon_api/_api_helpers/links.py similarity index 99% rename from ayon_api/_links.py rename to ayon_api/_api_helpers/links.py index 4c7a82237..7e75db966 100644 --- a/ayon_api/_links.py +++ b/ayon_api/_api_helpers/links.py @@ -4,20 +4,21 @@ import typing from typing import Optional, Any, Iterable -from .graphql_queries import ( +from ayon_api.graphql_queries import ( folders_graphql_query, tasks_graphql_query, products_graphql_query, versions_graphql_query, representations_graphql_query, ) -from ._base import _BaseServerAPI + +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import LinkDirection + from ayon_api.typing import LinkDirection -class _LinksAPI(_BaseServerAPI): +class LinksAPI(BaseServerAPI): def get_full_link_type_name( self, link_type_name: str, input_type: str, output_type: str ) -> str: diff --git a/ayon_api/_lists.py b/ayon_api/_api_helpers/lists.py similarity index 98% rename from ayon_api/_lists.py rename to ayon_api/_api_helpers/lists.py index 480c1e613..d52c99e7e 100644 --- a/ayon_api/_lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -2,19 +2,20 @@ import typing from typing import Optional, Iterable, Any, Dict, List, Generator -from ._base import _BaseServerAPI -from .utils import create_entity_id -from .graphql_queries import entity_lists_graphql_query +from ayon_api.utils import create_entity_id +from ayon_api.graphql_queries import entity_lists_graphql_query + +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ( + from ayon_api.typing import ( EntityListEntityType, EntityListAttributeDefinitionDict, EntityListItemMode, ) -class _ListsAPI(_BaseServerAPI): +class ListsAPI(BaseServerAPI): def get_entity_lists( self, project_name: str, diff --git a/ayon_api/_products.py b/ayon_api/_api_helpers/products.py similarity index 98% rename from ayon_api/_products.py rename to ayon_api/_api_helpers/products.py index eff1384f5..5ce78f97c 100644 --- a/ayon_api/_products.py +++ b/ayon_api/_api_helpers/products.py @@ -5,21 +5,22 @@ import typing from typing import Optional, Iterable, Generator, Any -from ._base import _BaseServerAPI, _PLACEHOLDER -from .utils import ( +from ayon_api.utils import ( prepare_list_filters, create_entity_id, ) -from .graphql_queries import ( +from ayon_api.graphql_queries import ( products_graphql_query, product_types_query, ) +from .base import BaseServerAPI, _PLACEHOLDER + if typing.TYPE_CHECKING: - from .typing import ProductDict, ProductTypeDict + from ayon_api.typing import ProductDict, ProductTypeDict -class _ProductsAPI(_BaseServerAPI): +class ProductsAPI(BaseServerAPI): def get_rest_product( self, project_name: str, product_id: str ) -> Optional["ProductDict"]: diff --git a/ayon_api/_projects.py b/ayon_api/_api_helpers/projects.py similarity index 98% rename from ayon_api/_projects.py rename to ayon_api/_api_helpers/projects.py index 9edc5948f..eb895f265 100644 --- a/ayon_api/_projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -4,16 +4,17 @@ import typing from typing import Optional, Generator, Iterable, Any -from ._base import _BaseServerAPI -from .constants import PROJECT_NAME_REGEX -from .utils import prepare_query_string, fill_own_attribs -from .graphql_queries import projects_graphql_query +from ayon_api.constants import PROJECT_NAME_REGEX +from ayon_api.utils import prepare_query_string, fill_own_attribs +from ayon_api.graphql_queries import projects_graphql_query + +from .base import BaseServerAPI if typing.TYPE_CHECKING: - from .typing import ProjectDict + from ayon_api.typing import ProjectDict -class _ProjectsAPI(_BaseServerAPI): +class ProjectsAPI(BaseServerAPI): def get_rest_project( self, project_name: str ) -> Optional["ProjectDict"]: diff --git a/ayon_api/_representations.py b/ayon_api/_api_helpers/representations.py similarity index 99% rename from ayon_api/_representations.py rename to ayon_api/_api_helpers/representations.py index a1d7d2ad1..e46353d09 100644 --- a/ayon_api/_representations.py +++ b/ayon_api/_api_helpers/representations.py @@ -5,24 +5,25 @@ import typing from typing import Optional, Iterable, Generator, Any -from ._base import _BaseServerAPI, _PLACEHOLDER -from .constants import REPRESENTATION_FILES_FIELDS -from .utils import ( +from ayon_api.constants import REPRESENTATION_FILES_FIELDS +from ayon_api.utils import ( RepresentationHierarchy, RepresentationParents, PatternType, create_entity_id, ) -from .graphql_queries import ( +from ayon_api.graphql_queries import ( representations_graphql_query, representations_hierarchy_qraphql_query, ) +from .base import BaseServerAPI, _PLACEHOLDER + if typing.TYPE_CHECKING: - from .typing import RepresentationDict + from ayon_api.typing import RepresentationDict -class _RepresentationsAPI(_BaseServerAPI): +class RepresentationsAPI(BaseServerAPI): def get_rest_representation( self, project_name: str, representation_id: str ) -> Optional["RepresentationDict"]: diff --git a/ayon_api/_tasks.py b/ayon_api/_api_helpers/tasks.py similarity index 99% rename from ayon_api/_tasks.py rename to ayon_api/_api_helpers/tasks.py index ed5f22fec..0e825de01 100644 --- a/ayon_api/_tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -3,23 +3,24 @@ import typing from typing import Optional, Iterable, Generator, Any -from ._base import _BaseServerAPI -from .utils import ( +from ayon_api.utils import ( prepare_list_filters, fill_own_attribs, create_entity_id, NOT_SET, ) -from .graphql_queries import ( +from ayon_api.graphql_queries import ( tasks_graphql_query, tasks_by_folder_paths_graphql_query, ) +from .base import BaseServerAPI + if typing.TYPE_CHECKING: - from .typing import TaskDict + from ayon_api.typing import TaskDict -class _TasksAPI(_BaseServerAPI): +class TasksAPI(BaseServerAPI): def get_rest_task( self, project_name: str, task_id: str ) -> Optional["TaskDict"]: diff --git a/ayon_api/_thumbnails.py b/ayon_api/_api_helpers/thumbnails.py similarity index 99% rename from ayon_api/_thumbnails.py rename to ayon_api/_api_helpers/thumbnails.py index 39fb86602..3265dd777 100644 --- a/ayon_api/_thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -2,16 +2,17 @@ import warnings from typing import Optional -from ._base import _BaseServerAPI -from .utils import ( +from ayon_api.utils import ( get_media_mime_type, ThumbnailContent, RequestTypes, RestApiResponse, ) +from .base import BaseServerAPI -class _ThumbnailsAPI(_BaseServerAPI): + +class ThumbnailsAPI(BaseServerAPI): def get_thumbnail_by_id( self, project_name: str, thumbnail_id: str ) -> ThumbnailContent: diff --git a/ayon_api/_versions.py b/ayon_api/_api_helpers/versions.py similarity index 98% rename from ayon_api/_versions.py rename to ayon_api/_api_helpers/versions.py index e805505a7..1302ed84b 100644 --- a/ayon_api/_versions.py +++ b/ayon_api/_api_helpers/versions.py @@ -4,20 +4,21 @@ import typing from typing import Optional, Iterable, Generator, Any -from ._base import _BaseServerAPI, _PLACEHOLDER -from .utils import ( +from ayon_api.utils import ( NOT_SET, create_entity_id, prepare_list_filters, ) -from .graphql import GraphQlQuery -from .graphql_queries import versions_graphql_query +from ayon_api.graphql import GraphQlQuery +from ayon_api.graphql_queries import versions_graphql_query + +from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: - from .typing import VersionDict + from ayon_api.typing import VersionDict -class _VersionsAPI(_BaseServerAPI): +class VersionsAPI(BaseServerAPI): def get_rest_version( self, project_name: str, version_id: str ) -> Optional["VersionDict"]: diff --git a/ayon_api/_workfiles.py b/ayon_api/_api_helpers/workfiles.py similarity index 96% rename from ayon_api/_workfiles.py rename to ayon_api/_api_helpers/workfiles.py index 2a624f607..8be0af6e3 100644 --- a/ayon_api/_workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -2,14 +2,15 @@ import typing from typing import Optional, Iterable, Generator -from ._base import _BaseServerAPI, _PLACEHOLDER -from .graphql_queries import workfiles_info_graphql_query +from ayon_api.graphql_queries import workfiles_info_graphql_query + +from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: - from .typing import WorkfileInfoDict + from ayon_api.typing import WorkfileInfoDict -class _WorkfilesAPI(_BaseServerAPI): +class WorkfilesAPI(BaseServerAPI): def get_workfiles_info( self, project_name: str, From af7b9091573e3b9fb9f69074455021bbce984099 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:04:19 +0200 Subject: [PATCH 149/506] use new imports --- ayon_api/server_api.py | 59 +++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 448ba1150..5eea9860e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -67,21 +67,22 @@ get_machine_name, fill_own_attribs, ) -from ._actions import _ActionsAPI -from ._activities import _ActivitiesAPI -from ._bundles_addons import _BundlesAddonsAPI -from ._events import _EventsAPI -from ._links import _LinksAPI -from ._lists import _ListsAPI -from ._projects import _ProjectsAPI -from ._folders import _FoldersAPI -from ._tasks import _TasksAPI -from ._products import _ProductsAPI -from ._versions import _VersionsAPI -from ._thumbnails import _ThumbnailsAPI -from ._workfiles import _WorkfilesAPI -from ._representations import _RepresentationsAPI - +from ._api_helpers import ( + ActionsAPI, + ActivitiesAPI, + BundlesAddonsAPI, + EventsAPI, + LinksAPI, + ListsAPI, + ProjectsAPI, + FoldersAPI, + TasksAPI, + ProductsAPI, + VersionsAPI, + ThumbnailsAPI, + WorkfilesAPI, + RepresentationsAPI, +) if typing.TYPE_CHECKING: from typing import Union @@ -214,20 +215,20 @@ def as_user(self, username): class ServerAPI( - _ActionsAPI, - _ActivitiesAPI, - _BundlesAddonsAPI, - _EventsAPI, - _ProjectsAPI, - _FoldersAPI, - _TasksAPI, - _ProductsAPI, - _VersionsAPI, - _RepresentationsAPI, - _WorkfilesAPI, - _LinksAPI, - _ListsAPI, - _ThumbnailsAPI, + ActionsAPI, + ActivitiesAPI, + BundlesAddonsAPI, + EventsAPI, + ProjectsAPI, + FoldersAPI, + TasksAPI, + ProductsAPI, + VersionsAPI, + RepresentationsAPI, + WorkfilesAPI, + LinksAPI, + ListsAPI, + ThumbnailsAPI, ): """Base handler of connection to server. From 9fe3b1c6638534d1a6e3d4aa15f6e3b838b0762b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:09:23 +0200 Subject: [PATCH 150/506] future annotations --- ayon_api/_api_helpers/actions.py | 71 +++++++++++++------------ ayon_api/_api_helpers/activities.py | 2 + ayon_api/_api_helpers/bundles_addons.py | 2 + ayon_api/_api_helpers/events.py | 2 + ayon_api/_api_helpers/folders.py | 4 +- ayon_api/_api_helpers/lists.py | 62 ++++++++++----------- ayon_api/_api_helpers/thumbnails.py | 2 + ayon_api/_api_helpers/workfiles.py | 2 + 8 files changed, 81 insertions(+), 66 deletions(-) diff --git a/ayon_api/_api_helpers/actions.py b/ayon_api/_api_helpers/actions.py index fcac238c4..6cbe9b7bd 100644 --- a/ayon_api/_api_helpers/actions.py +++ b/ayon_api/_api_helpers/actions.py @@ -1,13 +1,16 @@ +from __future__ import annotations + import typing -from typing import Optional, Dict, List, Any +from typing import Optional, Any from ayon_api.utils import prepare_query_string + from .base import BaseServerAPI if typing.TYPE_CHECKING: from ayon_api.typing import ( ActionEntityTypes, - ActionManifestDict, + ActionManifestdict, ActionTriggerResponse, ActionTakeResponse, ActionConfigResponse, @@ -21,13 +24,13 @@ def get_actions( self, project_name: Optional[str] = None, entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, mode: Optional["ActionModeType"] = None, - ) -> List["ActionManifestDict"]: + ) -> list["ActionManifestdict"]: """Get actions for a context. Args: @@ -35,16 +38,16 @@ def get_actions( actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. mode (Optional[ActionModeType]): Action modes. Returns: - List[ActionManifestDict]: List of action manifests. + list[ActionManifestdict]: list of action manifests. """ if variant is None: @@ -75,9 +78,9 @@ def trigger_action( addon_version: str, project_name: Optional[str] = None, entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, ) -> "ActionTriggerResponse": @@ -91,11 +94,11 @@ def trigger_action( actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. """ @@ -132,9 +135,9 @@ def get_action_config( addon_version: str, project_name: Optional[str] = None, entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, ) -> "ActionConfigResponse": @@ -148,11 +151,11 @@ def get_action_config( actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. Returns: @@ -177,12 +180,12 @@ def set_action_config( identifier: str, addon_name: str, addon_version: str, - value: Dict[str, Any], + value: dict[str, Any], project_name: Optional[str] = None, entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, ) -> "ActionConfigResponse": @@ -192,17 +195,17 @@ def set_action_config( identifier (str): Identifier of the action. addon_name (str): Name of the addon. addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action + value (Optional[dict[str, Any]]): Value of the action configuration. project_name (Optional[str]): Name of the project. None for global actions. entity_type (Optional[ActionEntityTypes]): Entity type where the action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the + entity_ids (Optional[list[str]]): list of entity ids where the action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes + entity_subtypes (Optional[list[str]]): list of entity subtypes folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. + form_data (Optional[dict[str, Any]]): Form data of the action. variant (Optional[str]): Settings variant. Returns: @@ -262,12 +265,12 @@ def _send_config_request( identifier: str, addon_name: str, addon_version: str, - value: Optional[Dict[str, Any]], + value: Optional[dict[str, Any]], project_name: Optional[str], entity_type: Optional["ActionEntityTypes"], - entity_ids: Optional[List[str]], - entity_subtypes: Optional[List[str]], - form_data: Optional[Dict[str, Any]], + entity_ids: Optional[list[str]], + entity_subtypes: Optional[list[str]], + form_data: Optional[dict[str, Any]], variant: Optional[str], ) -> "ActionConfigResponse": """Set and get action configuration.""" diff --git a/ayon_api/_api_helpers/activities.py b/ayon_api/_api_helpers/activities.py index 18ed4f411..c0c58406d 100644 --- a/ayon_api/_api_helpers/activities.py +++ b/ayon_api/_api_helpers/activities.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import json import typing from typing import Optional, Iterable, Generator, Any diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index 1b465f0bc..dfdd3b591 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import typing from typing import Optional, Any diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index 1c4e0e1cd..3c7d61273 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings import typing from typing import Optional, Any, Iterable, Generator diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index e43e798c3..7f6b3ca94 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -72,7 +72,7 @@ def get_rest_folders( in output. Slower to query. Returns: - List[FlatFolderDict]: List of folder entities. + list[FlatFolderDict]: List of folder entities. """ major, minor, patch, _, _ = self.get_server_version_tuple() @@ -187,7 +187,7 @@ def get_folders_rest( in output. Slower to query. Returns: - List[FlatFolderDict]: List of folder entities. + list[FlatFolderDict]: List of folder entities. """ warnings.warn( diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index d52c99e7e..f796723fd 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -1,6 +1,8 @@ +from __future__ import annotations + import json import typing -from typing import Optional, Iterable, Any, Dict, List, Generator +from typing import Optional, Iterable, Any, Generator from ayon_api.utils import create_entity_id from ayon_api.graphql_queries import entity_lists_graphql_query @@ -23,7 +25,7 @@ def get_entity_lists( list_ids: Optional[Iterable[str]] = None, active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, - ) -> Generator[Dict[str, Any], None, None]: + ) -> Generator[dict[str, Any], None, None]: """Fetch entity lists from server. Args: @@ -34,7 +36,7 @@ def get_entity_lists( fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Generator[Dict[str, Any], None, None]: Entity list entities + Generator[dict[str, Any], None, None]: Entity list entities matching defined filters. """ @@ -45,7 +47,7 @@ def get_entity_lists( if active is not None: fields.add("active") - filters: Dict[str, Any] = {"projectName": project_name} + filters: dict[str, Any] = {"projectName": project_name} if list_ids is not None: if not list_ids: return @@ -70,7 +72,7 @@ def get_entity_lists( def get_entity_list_rest( self, project_name: str, list_id: str - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get entity list by id using REST API. Args: @@ -78,7 +80,7 @@ def get_entity_list_rest( list_id (str): Entity list id. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[dict[str, Any]]: Entity list data or None if not found. """ response = self.get(f"projects/{project_name}/lists/{list_id}") @@ -90,7 +92,7 @@ def get_entity_list_by_id( project_name: str, list_id: str, fields: Optional[Iterable[str]] = None, - ) -> Optional[Dict[str, Any]]: + ) -> Optional[dict[str, Any]]: """Get entity list by id using GraphQl. Args: @@ -99,7 +101,7 @@ def get_entity_list_by_id( fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[dict[str, Any]]: Entity list data or None if not found. """ for entity_list in self.get_entity_lists( @@ -115,14 +117,14 @@ def create_entity_list( label: str, *, list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, + template: Optional[dict[str, Any]] = None, owner: Optional[str] = None, active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, + items: Optional[list[dict[str, Any]]] = None, list_id: Optional[str] = None, ) -> str: """Create entity list. @@ -181,10 +183,10 @@ def update_entity_list( list_id: str, *, label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -235,7 +237,7 @@ def delete_entity_list(self, project_name: str, list_id: str) -> None: def get_entity_list_attribute_definitions( self, project_name: str, list_id: str - ) -> List["EntityListAttributeDefinitionDict"]: + ) -> list["EntityListAttributeDefinitionDict"]: """Get attribute definitioins on entity list. Args: @@ -243,7 +245,7 @@ def get_entity_list_attribute_definitions( list_id (str): Entity list id. Returns: - List[EntityListAttributeDefinitionDict]: List of attribute + list[EntityListAttributeDefinitionDict]: List of attribute definitions. """ @@ -257,14 +259,14 @@ def set_entity_list_attribute_definitions( self, project_name: str, list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], + attribute_definitions: list["EntityListAttributeDefinitionDict"], ) -> None: """Set attribute definitioins on entity list. Args: project_name (str): Project name. list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): + attribute_definitions (list[EntityListAttributeDefinitionDict]): List of attribute definitions. """ @@ -281,9 +283,9 @@ def create_entity_list_item( *, position: Optional[int] = None, label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, item_id: Optional[str] = None, ) -> str: """Create entity list item. @@ -329,7 +331,7 @@ def update_entity_list_items( self, project_name: str, list_id: str, - items: List[Dict[str, Any]], + items: list[dict[str, Any]], mode: "EntityListItemMode", ) -> None: """Update items in entity list. @@ -337,7 +339,7 @@ def update_entity_list_items( Args: project_name (str): Project name where entity list live. list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. + items (list[dict[str, Any]]): Entity list items. mode (EntityListItemMode): Mode of items update. """ @@ -357,9 +359,9 @@ def update_entity_list_item( new_list_id: Optional[str], position: Optional[int] = None, label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, ) -> None: """Update item in entity list. diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 3265dd777..4e2242f2e 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import warnings from typing import Optional diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index 8be0af6e3..1be1c37a5 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import warnings import typing from typing import Optional, Iterable, Generator From 1448ffba77817527159fb06127d908c26fd9e6b3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:11:59 +0200 Subject: [PATCH 151/506] reorder imports --- ayon_api/_api_helpers/__init__.py | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py index 538a416d6..e102d82d4 100644 --- a/ayon_api/_api_helpers/__init__.py +++ b/ayon_api/_api_helpers/__init__.py @@ -1,34 +1,34 @@ from .base import BaseServerAPI -from .actions import ActionsAPI -from .activities import ActivitiesAPI from .bundles_addons import BundlesAddonsAPI from .events import EventsAPI -from .folders import FoldersAPI -from .links import LinksAPI -from .lists import ListsAPI -from .products import ProductsAPI from .projects import ProjectsAPI -from .representations import RepresentationsAPI +from .folders import FoldersAPI from .tasks import TasksAPI -from .thumbnails import ThumbnailsAPI +from .products import ProductsAPI from .versions import VersionsAPI +from .representations import RepresentationsAPI from .workfiles import WorkfilesAPI +from .thumbnails import ThumbnailsAPI +from .activities import ActivitiesAPI +from .actions import ActionsAPI +from .links import LinksAPI +from .lists import ListsAPI __all__ = ( "BaseServerAPI", - "ActionsAPI", - "ActivitiesAPI", "BundlesAddonsAPI", "EventsAPI", - "FoldersAPI", - "LinksAPI", - "ListsAPI", - "ProductsAPI", "ProjectsAPI", - "RepresentationsAPI", + "FoldersAPI", "TasksAPI", - "ThumbnailsAPI", + "ProductsAPI", "VersionsAPI", + "RepresentationsAPI", "WorkfilesAPI", + "ThumbnailsAPI", + "ActivitiesAPI", + "ActionsAPI", + "LinksAPI", + "ListsAPI", ) From 32bbda92aaaa2689c419c24911fe9b0e1e2bb907 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:42:17 +0200 Subject: [PATCH 152/506] move anatomy and roots methods to project api --- ayon_api/_api_helpers/projects.py | 292 +++++++++++++++++++++++++++++- ayon_api/server_api.py | 291 ----------------------------- ayon_api/typing.py | 3 + 3 files changed, 294 insertions(+), 292 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index eb895f265..f24a67f3c 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -1,6 +1,8 @@ from __future__ import annotations import json +import platform +import warnings import typing from typing import Optional, Generator, Iterable, Any @@ -11,10 +13,99 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from ayon_api.typing import ProjectDict + from ayon_api.typing import ProjectDict, AnatomyPresetDict class ProjectsAPI(BaseServerAPI): + def get_project_anatomy_presets(self) -> list["AnatomyPresetDict"]: + """Anatomy presets available on server. + + Content has basic information about presets. Example output:: + + [ + { + "name": "netflix_VFX", + "primary": false, + "version": "1.0.0" + }, + { + ... + }, + ... + ] + + Returns: + list[dict[str, str]]: Anatomy presets available on server. + + """ + result = self.get("anatomy/presets") + result.raise_for_status() + return result.data.get("presets") or [] + + def get_default_anatomy_preset_name(self) -> str: + """Name of default anatomy preset. + + Primary preset is used as default preset. But when primary preset is + not set a built-in is used instead. Built-in preset is named '_'. + + Returns: + str: Name of preset that can be used by + 'get_project_anatomy_preset'. + + """ + for preset in self.get_project_anatomy_presets(): + if preset.get("primary"): + return preset["name"] + return "_" + + def get_project_anatomy_preset( + self, preset_name: Optional[str] = None + ) -> "AnatomyPresetDict": + """Anatomy preset values by name. + + Get anatomy preset values by preset name. Primary preset is returned + if preset name is set to 'None'. + + Args: + preset_name (Optional[str]): Preset name. + + Returns: + AnatomyPresetDict: Anatomy preset values. + + """ + if preset_name is None: + preset_name = "__primary__" + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 0, 8): + preset_name = self.get_default_anatomy_preset_name() + + result = self.get(f"anatomy/presets/{preset_name}") + result.raise_for_status() + return result.data + + def get_built_in_anatomy_preset(self) -> "AnatomyPresetDict": + """Get built-in anatomy preset. + + Returns: + AnatomyPresetDict: Built-in anatomy preset. + + """ + preset_name = "__builtin__" + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 0, 8): + preset_name = "_" + return self.get_project_anatomy_preset(preset_name) + + def get_build_in_anatomy_preset(self) -> "AnatomyPresetDict": + warnings.warn( + ( + "Used deprecated 'get_build_in_anatomy_preset' use" + " 'get_built_in_anatomy_preset' instead." + ), + DeprecationWarning + ) + return self.get_built_in_anatomy_preset() + def get_rest_project( self, project_name: str ) -> Optional["ProjectDict"]: @@ -338,6 +429,160 @@ def delete_project(self, project_name: str): f"Failed to delete project \"{project_name}\". {detail}" ) + def get_project_root_overrides( + self, project_name: str + ) -> dict[str, dict[str, str]]: + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. + + Args: + project_name (str): Name of project. + + Returns: + dict[str, dict[str, str]]: Root values by root name by site id. + + """ + result = self.get(f"projects/{project_name}/roots") + result.raise_for_status() + return result.data + + def get_project_roots_by_site( + self, project_name: str + ) -> dict[str, dict[str, str]]: + """Root overrides per site name. + + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. + + Deprecated: + Use 'get_project_root_overrides' instead. Function + deprecated since 1.0.6 + + Args: + project_name (str): Name of project. + + Returns: + dict[str, dict[str, str]]: Root values by root name by site id. + + """ + warnings.warn( + ( + "Method 'get_project_roots_by_site' is deprecated." + " Please use 'get_project_root_overrides' instead." + ), + DeprecationWarning + ) + return self.get_project_root_overrides(project_name) + + def get_project_root_overrides_by_site_id( + self, project_name: str, site_id: Optional[str] = None + ) -> dict[str, str]: + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + + Args: + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. + + Returns: + dict[str, str]: Root values by root name or None if + site does not have overrides. + + """ + if site_id is None: + site_id = self.get_site_id() + + if site_id is None: + return {} + roots = self.get_project_root_overrides(project_name) + return roots.get(site_id, {}) + + def get_project_roots_for_site( + self, project_name: str, site_id: Optional[str] = None + ) -> dict[str, str]: + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + + Deprecated: + Use 'get_project_root_overrides_by_site_id' instead. Function + deprecated since 1.0.6 + Args: + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. + + Returns: + dict[str, str]: Root values by root name, root name is not + available if it does not have overrides. + + """ + warnings.warn( + ( + "Method 'get_project_roots_for_site' is deprecated." + " Please use 'get_project_root_overrides_by_site_id' instead." + ), + DeprecationWarning + ) + return self.get_project_root_overrides_by_site_id(project_name) + + def get_project_roots_by_site_id( + self, project_name: str, site_id: Optional[str] = None + ) -> dict[str, str]: + """Root values for a site. + + If site id is not passed a site set in current api object is used + instead. If site id is not available, default roots are returned + for current platform. + + Args: + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + root values. + + Returns: + dict[str, str]: Root values. + + """ + if site_id is None: + site_id = self.get_site_id() + + return self._get_project_roots_values(project_name, site_id=site_id) + + def get_project_roots_by_platform( + self, project_name: str, platform_name: Optional[str] = None + ) -> dict[str, str]: + """Root values for a site. + + If platform name is not passed current platform name is used instead. + + This function does return root values without site overrides. It is + possible to use the function to receive default root values. + + Args: + project_name (str): Name of project. + platform_name (Optional[Literal["windows", "linux", "darwin"]]): + Platform name for which want to receive root values. Current + platform name is used if not passed. + + Returns: + dict[str, str]: Root values. + + """ + return self._get_project_roots_values( + project_name, platform_name=platform_name + ) + def _get_project_graphql_fields( self, fields: Optional[set[str]] ) -> tuple[set[str], bool]: @@ -445,3 +690,48 @@ def _get_graphql_projects( fill_own_attribs(project) self._fill_project_entity_data(project) yield project + + def _get_project_roots_values( + self, + project_name: str, + site_id: Optional[str] = None, + platform_name: Optional[str] = None, + ) -> dict[str, str]: + """Root values for site or platform. + + Helper function that treats 'siteRoots' endpoint. The endpoint + requires to pass exactly one query value of site id + or platform name. + + When using platform name, it does return default project roots without + any site overrides. + + Output should contain all project roots with all filled values. If + value does not have override on a site, it should be filled with + project default value. + + Args: + project_name (str): Project name. + site_id (Optional[str]): Site id for which want to receive + site overrides. + platform_name (Optional[str]): Platform for which want to receive + roots. + + Returns: + dict[str, str]: Root values. + + """ + query_data = {} + if site_id is not None: + query_data["site_id"] = site_id + else: + if platform_name is None: + platform_name = platform.system() + query_data["platform"] = platform_name.lower() + + query = prepare_query_string(query_data) + response = self.get( + f"projects/{project_name}/siteRoots{query}" + ) + response.raise_for_status() + return response.data \ No newline at end of file diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 5eea9860e..a8c819865 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -12,7 +12,6 @@ import time import logging import platform -import copy import uuid import warnings from contextlib import contextmanager @@ -94,7 +93,6 @@ AttributesSchemaDict, InstallersInfoDict, DependencyPackagesDict, - AnatomyPresetDict, SecretDict, AnyEntityDict, @@ -2356,295 +2354,6 @@ def upload_dependency_package( route = self._get_dependency_package_route(dst_filename) self.upload_file(route, src_filepath, progress=progress) - # Anatomy presets - def get_project_anatomy_presets(self) -> list["AnatomyPresetDict"]: - """Anatomy presets available on server. - - Content has basic information about presets. Example output:: - - [ - { - "name": "netflix_VFX", - "primary": false, - "version": "1.0.0" - }, - { - ... - }, - ... - ] - - Returns: - list[dict[str, str]]: Anatomy presets available on server. - - """ - result = self.get("anatomy/presets") - result.raise_for_status() - return result.data.get("presets") or [] - - def get_default_anatomy_preset_name(self) -> str: - """Name of default anatomy preset. - - Primary preset is used as default preset. But when primary preset is - not set a built-in is used instead. Built-in preset is named '_'. - - Returns: - str: Name of preset that can be used by - 'get_project_anatomy_preset'. - - """ - for preset in self.get_project_anatomy_presets(): - if preset.get("primary"): - return preset["name"] - return "_" - - def get_project_anatomy_preset( - self, preset_name: Optional[str] = None - ) -> "AnatomyPresetDict": - """Anatomy preset values by name. - - Get anatomy preset values by preset name. Primary preset is returned - if preset name is set to 'None'. - - Args: - preset_name (Optional[str]): Preset name. - - Returns: - AnatomyPresetDict: Anatomy preset values. - - """ - if preset_name is None: - preset_name = "__primary__" - major, minor, patch, _, _ = self.server_version_tuple - if (major, minor, patch) < (1, 0, 8): - preset_name = self.get_default_anatomy_preset_name() - - result = self.get(f"anatomy/presets/{preset_name}") - result.raise_for_status() - return result.data - - def get_built_in_anatomy_preset(self) -> "AnatomyPresetDict": - """Get built-in anatomy preset. - - Returns: - AnatomyPresetDict: Built-in anatomy preset. - - """ - preset_name = "__builtin__" - major, minor, patch, _, _ = self.server_version_tuple - if (major, minor, patch) < (1, 0, 8): - preset_name = "_" - return self.get_project_anatomy_preset(preset_name) - - def get_build_in_anatomy_preset(self) -> "AnatomyPresetDict": - warnings.warn( - ( - "Used deprecated 'get_build_in_anatomy_preset' use" - " 'get_built_in_anatomy_preset' instead." - ), - DeprecationWarning - ) - return self.get_built_in_anatomy_preset() - - def get_project_root_overrides( - self, project_name: str - ) -> dict[str, dict[str, str]]: - """Root overrides per site name. - - Method is based on logged user and can't be received for any other - user on server. - - Output will contain only roots per site id used by logged user. - - Args: - project_name (str): Name of project. - - Returns: - dict[str, dict[str, str]]: Root values by root name by site id. - - """ - result = self.get(f"projects/{project_name}/roots") - result.raise_for_status() - return result.data - - def get_project_roots_by_site( - self, project_name: str - ) -> dict[str, dict[str, str]]: - """Root overrides per site name. - - Method is based on logged user and can't be received for any other - user on server. - - Output will contain only roots per site id used by logged user. - - Deprecated: - Use 'get_project_root_overrides' instead. Function - deprecated since 1.0.6 - - Args: - project_name (str): Name of project. - - Returns: - dict[str, dict[str, str]]: Root values by root name by site id. - - """ - warnings.warn( - ( - "Method 'get_project_roots_by_site' is deprecated." - " Please use 'get_project_root_overrides' instead." - ), - DeprecationWarning - ) - return self.get_project_root_overrides(project_name) - - def get_project_root_overrides_by_site_id( - self, project_name: str, site_id: Optional[str] = None - ) -> dict[str, str]: - """Root overrides for site. - - If site id is not passed a site set in current api object is used - instead. - - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. - - Returns: - dict[str, str]: Root values by root name or None if - site does not have overrides. - - """ - if site_id is None: - site_id = self.site_id - - if site_id is None: - return {} - roots = self.get_project_root_overrides(project_name) - return roots.get(site_id, {}) - - def get_project_roots_for_site( - self, project_name: str, site_id: Optional[str] = None - ) -> dict[str, str]: - """Root overrides for site. - - If site id is not passed a site set in current api object is used - instead. - - Deprecated: - Use 'get_project_root_overrides_by_site_id' instead. Function - deprecated since 1.0.6 - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. - - Returns: - dict[str, str]: Root values by root name, root name is not - available if it does not have overrides. - - """ - warnings.warn( - ( - "Method 'get_project_roots_for_site' is deprecated." - " Please use 'get_project_root_overrides_by_site_id' instead." - ), - DeprecationWarning - ) - return self.get_project_root_overrides_by_site_id(project_name) - - def _get_project_roots_values( - self, - project_name: str, - site_id: Optional[str] = None, - platform_name: Optional[str] = None, - ) -> dict[str, str]: - """Root values for site or platform. - - Helper function that treats 'siteRoots' endpoint. The endpoint - requires to pass exactly one query value of site id - or platform name. - - When using platform name, it does return default project roots without - any site overrides. - - Output should contain all project roots with all filled values. If - value does not have override on a site, it should be filled with - project default value. - - Args: - project_name (str): Project name. - site_id (Optional[str]): Site id for which want to receive - site overrides. - platform_name (Optional[str]): Platform for which want to receive - roots. - - Returns: - dict[str, str]: Root values. - - """ - query_data = {} - if site_id is not None: - query_data["site_id"] = site_id - else: - if platform_name is None: - platform_name = platform.system() - query_data["platform"] = platform_name.lower() - - query = prepare_query_string(query_data) - response = self.get( - f"projects/{project_name}/siteRoots{query}" - ) - response.raise_for_status() - return response.data - - def get_project_roots_by_site_id( - self, project_name: str, site_id: Optional[str] = None - ) -> dict[str, str]: - """Root values for a site. - - If site id is not passed a site set in current api object is used - instead. If site id is not available, default roots are returned - for current platform. - - Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - root values. - - Returns: - dict[str, str]: Root values. - - """ - if site_id is None: - site_id = self.site_id - - return self._get_project_roots_values(project_name, site_id=site_id) - - def get_project_roots_by_platform( - self, project_name: str, platform_name: Optional[str] = None - ) -> dict[str, str]: - """Root values for a site. - - If platform name is not passed current platform name is used instead. - - This function does return root values without site overrides. It is - possible to use the function to receive default root values. - - Args: - project_name (str): Name of project. - platform_name (Optional[Literal["windows", "linux", "darwin"]]): - Platform name for which want to receive root values. Current - platform name is used if not passed. - - Returns: - dict[str, str]: Root values. - - """ - return self._get_project_roots_values( - project_name, platform_name=platform_name - ) - def get_secrets(self) -> list["SecretDict"]: """Get all secrets. diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 038372f0c..9f45a5120 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -312,12 +312,15 @@ class AnatomyPresetDict(TypedDict): link_types: list[AnatomyPresetLinkTypeDict] statuses: list[AnatomyPresetStatusDict] tags: list[AnatomyPresetTagDict] + primary: bool + name: str class SecretDict(TypedDict): name: str value: str + ProjectDict = dict[str, Any] FolderDict = dict[str, Any] TaskDict = dict[str, Any] From ea0632a4dd6bc7118a47a262543585bfd965c245 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:44:33 +0200 Subject: [PATCH 153/506] separated attributes methods --- automated_api.py | 16 +-- ayon_api/_api_helpers/__init__.py | 2 + ayon_api/_api_helpers/attributes.py | 159 ++++++++++++++++++++++++++++ ayon_api/server_api.py | 145 +------------------------ 4 files changed, 172 insertions(+), 150 deletions(-) create mode 100644 ayon_api/_api_helpers/attributes.py diff --git a/automated_api.py b/automated_api.py index d6a24b95f..e0514a6ed 100644 --- a/automated_api.py +++ b/automated_api.py @@ -339,16 +339,17 @@ def prepare_api_functions(api_globals): ActivitiesAPI, BundlesAddonsAPI, EventsAPI, + AttributesAPI, + ProjectsAPI, FoldersAPI, TasksAPI, ProductsAPI, VersionsAPI, + RepresentationsAPI, + WorkfilesAPI, LinksAPI, ListsAPI, - ProjectsAPI, ThumbnailsAPI, - WorkfilesAPI, - RepresentationsAPI, ) functions = [] @@ -357,16 +358,17 @@ def prepare_api_functions(api_globals): _items.extend(ActivitiesAPI.__dict__.items()) _items.extend(BundlesAddonsAPI.__dict__.items()) _items.extend(EventsAPI.__dict__.items()) - _items.extend(LinksAPI.__dict__.items()) - _items.extend(ListsAPI.__dict__.items()) + _items.extend(AttributesAPI.__dict__.items()) _items.extend(ProjectsAPI.__dict__.items()) _items.extend(FoldersAPI.__dict__.items()) _items.extend(TasksAPI.__dict__.items()) _items.extend(ProductsAPI.__dict__.items()) _items.extend(VersionsAPI.__dict__.items()) - _items.extend(ThumbnailsAPI.__dict__.items()) - _items.extend(WorkfilesAPI.__dict__.items()) _items.extend(RepresentationsAPI.__dict__.items()) + _items.extend(WorkfilesAPI.__dict__.items()) + _items.extend(LinksAPI.__dict__.items()) + _items.extend(ListsAPI.__dict__.items()) + _items.extend(ThumbnailsAPI.__dict__.items()) processed = set() for attr_name, attr in _items: diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py index e102d82d4..42b834ee7 100644 --- a/ayon_api/_api_helpers/__init__.py +++ b/ayon_api/_api_helpers/__init__.py @@ -1,6 +1,7 @@ from .base import BaseServerAPI from .bundles_addons import BundlesAddonsAPI from .events import EventsAPI +from .attributes import AttributesAPI from .projects import ProjectsAPI from .folders import FoldersAPI from .tasks import TasksAPI @@ -19,6 +20,7 @@ "BaseServerAPI", "BundlesAddonsAPI", "EventsAPI", + "AttributesAPI", "ProjectsAPI", "FoldersAPI", "TasksAPI", diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py new file mode 100644 index 000000000..9a9216d0f --- /dev/null +++ b/ayon_api/_api_helpers/attributes.py @@ -0,0 +1,159 @@ +from __future__ import annotations + +import typing +from typing import Optional +import copy + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import ( + AttributeSchemaDataDict, + AttributeSchemaDict, + AttributesSchemaDict, + AttributeScope, + ) + + +class AttributesAPI(BaseServerAPI): + _attributes_schema = None + _entity_type_attributes_cache = {} + + def get_attributes_schema( + self, use_cache: bool = True + ) -> "AttributesSchemaDict": + if not use_cache: + self.reset_attributes_schema() + + if self._attributes_schema is None: + result = self.get("attributes") + result.raise_for_status() + self._attributes_schema = result.data + return copy.deepcopy(self._attributes_schema) + + def reset_attributes_schema(self): + self._attributes_schema = None + self._entity_type_attributes_cache = {} + + def set_attribute_config( + self, + attribute_name: str, + data: "AttributeSchemaDataDict", + scope: list["AttributeScope"], + position: Optional[int] = None, + builtin: bool = False, + ): + if position is None: + attributes = self.get("attributes").data["attributes"] + origin_attr = next( + ( + attr for attr in attributes + if attr["name"] == attribute_name + ), + None + ) + if origin_attr: + position = origin_attr["position"] + else: + position = len(attributes) + + response = self.put( + f"attributes/{attribute_name}", + data=data, + scope=scope, + position=position, + builtin=builtin + ) + if response.status_code != 204: + # TODO raise different exception + raise ValueError( + f"Attribute \"{attribute_name}\" was not created/updated." + f" {response.detail}" + ) + + self.reset_attributes_schema() + + def remove_attribute_config(self, attribute_name: str): + """Remove attribute from server. + + This can't be un-done, please use carefully. + + Args: + attribute_name (str): Name of attribute to remove. + + """ + response = self.delete(f"attributes/{attribute_name}") + response.raise_for_status( + f"Attribute \"{attribute_name}\" was not created/updated." + f" {response.detail}" + ) + + self.reset_attributes_schema() + + def get_attributes_for_type( + self, entity_type: "AttributeScope" + ) -> dict[str, "AttributeSchemaDict"]: + """Get attribute schemas available for an entity type. + + Example:: + + ``` + # Example attribute schema + { + # Common + "type": "integer", + "title": "Clip Out", + "description": null, + "example": 1, + "default": 1, + # These can be filled based on value of 'type' + "gt": null, + "ge": null, + "lt": null, + "le": null, + "minLength": null, + "maxLength": null, + "minItems": null, + "maxItems": null, + "regex": null, + "enum": null + } + ``` + + Args: + entity_type (str): Entity type for which should be attributes + received. + + Returns: + dict[str, dict[str, Any]]: Attribute schemas that are available + for entered entity type. + + """ + attributes = self._entity_type_attributes_cache.get(entity_type) + if attributes is None: + attributes_schema = self.get_attributes_schema() + attributes = {} + for attr in attributes_schema["attributes"]: + if entity_type not in attr["scope"]: + continue + attr_name = attr["name"] + attributes[attr_name] = attr["data"] + + self._entity_type_attributes_cache[entity_type] = attributes + + return copy.deepcopy(attributes) + + def get_attributes_fields_for_type( + self, entity_type: "AttributeScope" + ) -> set[str]: + """Prepare attribute fields for entity type. + + Returns: + set[str]: Attributes fields for entity type. + + """ + attributes = self.get_attributes_for_type(entity_type) + return { + f"attrib.{attr}" + for attr in attributes + } diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a8c819865..bc84fbf6e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -71,6 +71,7 @@ ActivitiesAPI, BundlesAddonsAPI, EventsAPI, + AttributesAPI, LinksAPI, ListsAPI, ProjectsAPI, @@ -87,10 +88,6 @@ from typing import Union from .typing import ( ServerVersion, - AttributeScope, - AttributeSchemaDataDict, - AttributeSchemaDict, - AttributesSchemaDict, InstallersInfoDict, DependencyPackagesDict, SecretDict, @@ -217,6 +214,7 @@ class ServerAPI( ActivitiesAPI, BundlesAddonsAPI, EventsAPI, + AttributesAPI, ProjectsAPI, FoldersAPI, TasksAPI, @@ -1775,145 +1773,6 @@ def get_schemas(self) -> dict[str, Any]: server_schema = self.get_server_schema() return server_schema["components"]["schemas"] - def get_attributes_schema( - self, use_cache: bool = True - ) -> "AttributesSchemaDict": - if not use_cache: - self.reset_attributes_schema() - - if self._attributes_schema is None: - result = self.get("attributes") - result.raise_for_status() - self._attributes_schema = result.data - return copy.deepcopy(self._attributes_schema) - - def reset_attributes_schema(self): - self._attributes_schema = None - self._entity_type_attributes_cache = {} - - def set_attribute_config( - self, - attribute_name: str, - data: "AttributeSchemaDataDict", - scope: list["AttributeScope"], - position: Optional[int] = None, - builtin: bool = False, - ): - if position is None: - attributes = self.get("attributes").data["attributes"] - origin_attr = next( - ( - attr for attr in attributes - if attr["name"] == attribute_name - ), - None - ) - if origin_attr: - position = origin_attr["position"] - else: - position = len(attributes) - - response = self.put( - f"attributes/{attribute_name}", - data=data, - scope=scope, - position=position, - builtin=builtin - ) - if response.status_code != 204: - # TODO raise different exception - raise ValueError( - f"Attribute \"{attribute_name}\" was not created/updated." - f" {response.detail}" - ) - - self.reset_attributes_schema() - - def remove_attribute_config(self, attribute_name: str): - """Remove attribute from server. - - This can't be un-done, please use carefully. - - Args: - attribute_name (str): Name of attribute to remove. - - """ - response = self.delete(f"attributes/{attribute_name}") - response.raise_for_status( - f"Attribute \"{attribute_name}\" was not created/updated." - f" {response.detail}" - ) - - self.reset_attributes_schema() - - def get_attributes_for_type( - self, entity_type: "AttributeScope" - ) -> dict[str, "AttributeSchemaDict"]: - """Get attribute schemas available for an entity type. - - Example:: - - ``` - # Example attribute schema - { - # Common - "type": "integer", - "title": "Clip Out", - "description": null, - "example": 1, - "default": 1, - # These can be filled based on value of 'type' - "gt": null, - "ge": null, - "lt": null, - "le": null, - "minLength": null, - "maxLength": null, - "minItems": null, - "maxItems": null, - "regex": null, - "enum": null - } - ``` - - Args: - entity_type (str): Entity type for which should be attributes - received. - - Returns: - dict[str, dict[str, Any]]: Attribute schemas that are available - for entered entity type. - - """ - attributes = self._entity_type_attributes_cache.get(entity_type) - if attributes is None: - attributes_schema = self.get_attributes_schema() - attributes = {} - for attr in attributes_schema["attributes"]: - if entity_type not in attr["scope"]: - continue - attr_name = attr["name"] - attributes[attr_name] = attr["data"] - - self._entity_type_attributes_cache[entity_type] = attributes - - return copy.deepcopy(attributes) - - def get_attributes_fields_for_type( - self, entity_type: "AttributeScope" - ) -> set[str]: - """Prepare attribute fields for entity type. - - Returns: - set[str]: Attributes fields for entity type. - - """ - attributes = self.get_attributes_for_type(entity_type) - return { - f"attrib.{attr}" - for attr in attributes - } - def get_default_fields_for_type(self, entity_type: str) -> set[str]: """Default fields for entity type. From 2387765ec1982046e7324bd3654330b5e896c6c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 11:58:02 +0200 Subject: [PATCH 154/506] moved dependency packages and installers endpoints to separate files --- automated_api.py | 10 +- ayon_api/_api_helpers/__init__.py | 4 + ayon_api/_api_helpers/dependency_packages.py | 237 +++++++++++ ayon_api/_api_helpers/installers.py | 168 ++++++++ ayon_api/server_api.py | 402 +------------------ 5 files changed, 429 insertions(+), 392 deletions(-) create mode 100644 ayon_api/_api_helpers/dependency_packages.py create mode 100644 ayon_api/_api_helpers/installers.py diff --git a/automated_api.py b/automated_api.py index e0514a6ed..112e904a1 100644 --- a/automated_api.py +++ b/automated_api.py @@ -335,8 +335,8 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): def prepare_api_functions(api_globals): from ayon_api.server_api import ( # noqa: E402 ServerAPI, - ActionsAPI, - ActivitiesAPI, + InstallersAPI, + DependencyPackagesAPI, BundlesAddonsAPI, EventsAPI, AttributesAPI, @@ -347,13 +347,17 @@ def prepare_api_functions(api_globals): VersionsAPI, RepresentationsAPI, WorkfilesAPI, + ThumbnailsAPI, + ActivitiesAPI, + ActionsAPI, LinksAPI, ListsAPI, - ThumbnailsAPI, ) functions = [] _items = list(ServerAPI.__dict__.items()) + _items.extend(InstallersAPI.__dict__.items()) + _items.extend(DependencyPackagesAPI.__dict__.items()) _items.extend(ActionsAPI.__dict__.items()) _items.extend(ActivitiesAPI.__dict__.items()) _items.extend(BundlesAddonsAPI.__dict__.items()) diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py index 42b834ee7..0938461be 100644 --- a/ayon_api/_api_helpers/__init__.py +++ b/ayon_api/_api_helpers/__init__.py @@ -1,4 +1,6 @@ from .base import BaseServerAPI +from .installers import InstallersAPI +from .dependency_packages import DependencyPackagesAPI from .bundles_addons import BundlesAddonsAPI from .events import EventsAPI from .attributes import AttributesAPI @@ -18,6 +20,8 @@ __all__ = ( "BaseServerAPI", + "InstallersAPI", + "DependencyPackagesAPI", "BundlesAddonsAPI", "EventsAPI", "AttributesAPI", diff --git a/ayon_api/_api_helpers/dependency_packages.py b/ayon_api/_api_helpers/dependency_packages.py new file mode 100644 index 000000000..7268d8a77 --- /dev/null +++ b/ayon_api/_api_helpers/dependency_packages.py @@ -0,0 +1,237 @@ +from __future__ import annotations + +import os +import warnings +import platform +import typing +from typing import Optional, Any + +from ayon_api.utils import TransferProgress + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import DependencyPackagesDict + + +class DependencyPackagesAPI(BaseServerAPI): + def get_dependency_packages(self) -> "DependencyPackagesDict": + """Information about dependency packages on server. + + To download dependency package, use 'download_dependency_package' + method and pass in 'filename'. + + Example data structure:: + + { + "packages": [ + { + "filename": str, + "platform": str, + "checksum": str, + "checksumAlgorithm": str, + "size": int, + "sources": list[dict[str, Any]], + "supportedAddons": dict[str, str], + "pythonModules": dict[str, str] + } + ] + } + + Returns: + DependencyPackagesDict: Information about dependency packages + known for server. + + """ + endpoint = self._get_dependency_package_route() + result = self.get(endpoint) + result.raise_for_status() + return result.data + + def create_dependency_package( + self, + filename: str, + python_modules: dict[str, str], + source_addons: dict[str, str], + installer_version: str, + checksum: str, + checksum_algorithm: str, + file_size: int, + sources: Optional[list[dict[str, Any]]] = None, + platform_name: Optional[str] = None, + ): + """Create dependency package on server. + + The package will be created on a server, it is also required to upload + the package archive file (using :meth:`upload_dependency_package`). + + Args: + filename (str): Filename of dependency package. + python_modules (dict[str, str]): Python modules in dependency + package:: + + {"": "", ...} + + source_addons (dict[str, str]): Name of addons for which is + dependency package created:: + + {"": "", ...} + + installer_version (str): Version of installer for which was + package created. + checksum (str): Checksum of archive file where dependencies are. + checksum_algorithm (str): Algorithm used to calculate checksum. + file_size (Optional[int]): Size of file. + sources (Optional[list[dict[str, Any]]]): Information about + sources from where it is possible to get file. + platform_name (Optional[str]): Name of platform for which is + dependency package targeted. Default value is + current platform. + + """ + post_body = { + "filename": filename, + "pythonModules": python_modules, + "sourceAddons": source_addons, + "installerVersion": installer_version, + "checksum": checksum, + "checksumAlgorithm": checksum_algorithm, + "size": file_size, + "platform": platform_name or platform.system().lower(), + } + if sources: + post_body["sources"] = sources + + route = self._get_dependency_package_route() + response = self.post(route, **post_body) + response.raise_for_status() + + def update_dependency_package( + self, filename: str, sources: list[dict[str, Any]] + ): + """Update dependency package metadata on server. + + Args: + filename (str): Filename of dependency package. + sources (list[dict[str, Any]]): Information about + sources from where it is possible to get file. Fully replaces + existing sources. + + """ + response = self.patch( + self._get_dependency_package_route(filename), + sources=sources + ) + response.raise_for_status() + + def delete_dependency_package( + self, filename: str, platform_name: Optional[str] = None + ): + """Remove dependency package for specific platform. + + Args: + filename (str): Filename of dependency package. + platform_name (Optional[str]): Deprecated. + + """ + if platform_name is not None: + warnings.warn( + ( + "Argument 'platform_name' is deprecated in" + " 'delete_dependency_package'. The argument will be" + " removed, please modify your code accordingly." + ), + DeprecationWarning + ) + + route = self._get_dependency_package_route(filename) + response = self.delete(route) + response.raise_for_status("Failed to delete dependency file") + return response.data + + def download_dependency_package( + self, + src_filename: str, + dst_directory: str, + dst_filename: str, + platform_name: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> str: + """Download dependency package from server. + + This method requires to have authorized token available. The package + is only downloaded. + + Args: + src_filename (str): Filename of dependency pacakge. + For server version 0.2.0 and lower it is name of package + to download. + dst_directory (str): Where the file should be downloaded. + dst_filename (str): Name of destination filename. + platform_name (Optional[str]): Deprecated. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + str: Filepath to downloaded file. + + """ + if platform_name is not None: + warnings.warn( + ( + "Argument 'platform_name' is deprecated in" + " 'download_dependency_package'. The argument will be" + " removed, please modify your code accordingly." + ), + DeprecationWarning + ) + route = self._get_dependency_package_route(src_filename) + package_filepath = os.path.join(dst_directory, dst_filename) + self.download_file( + route, + package_filepath, + chunk_size=chunk_size, + progress=progress + ) + return package_filepath + + def upload_dependency_package( + self, + src_filepath: str, + dst_filename: str, + platform_name: Optional[str] = None, + progress: Optional[TransferProgress] = None, + ): + """Upload dependency package to server. + + Args: + src_filepath (str): Path to a package file. + dst_filename (str): Dependency package filename or name of package + for server version 0.2.0 or lower. Must be unique. + platform_name (Optional[str]): Deprecated. + progress (Optional[TransferProgress]): Object to keep track about + upload state. + + """ + if platform_name is not None: + warnings.warn( + ( + "Argument 'platform_name' is deprecated in" + " 'upload_dependency_package'. The argument will be" + " removed, please modify your code accordingly." + ), + DeprecationWarning + ) + + route = self._get_dependency_package_route(dst_filename) + self.upload_file(route, src_filepath, progress=progress) + + def _get_dependency_package_route( + self, filename: Optional[str] = None + ) -> str: + endpoint = "desktop/dependencyPackages" + if filename: + return f"{endpoint}/{filename}" + return endpoint diff --git a/ayon_api/_api_helpers/installers.py b/ayon_api/_api_helpers/installers.py new file mode 100644 index 000000000..f6d7ec7f5 --- /dev/null +++ b/ayon_api/_api_helpers/installers.py @@ -0,0 +1,168 @@ +from __future__ import annotations + + +import typing +from typing import Optional, Any + +from ayon_api.utils import prepare_query_string, TransferProgress + +from .base import BaseServerAPI + +if typing.TYPE_CHECKING: + from ayon_api.typing import InstallersInfoDict + + +class InstallersAPI(BaseServerAPI): + def get_installers( + self, + version: Optional[str] = None, + platform_name: Optional[str] = None, + ) -> "InstallersInfoDict": + """Information about desktop application installers on server. + + Desktop application installers are helpers to download/update AYON + desktop application for artists. + + Args: + version (Optional[str]): Filter installers by version. + platform_name (Optional[str]): Filter installers by platform name. + + Returns: + InstallersInfoDict: Information about installers known for server. + + """ + query = prepare_query_string({ + "version": version or None, + "platform": platform_name or None, + }) + response = self.get(f"desktop/installers{query}") + response.raise_for_status() + return response.data + + def create_installer( + self, + filename: str, + version: str, + python_version: str, + platform_name: str, + python_modules: dict[str, str], + runtime_python_modules: dict[str, str], + checksum: str, + checksum_algorithm: str, + file_size: int, + sources: Optional[list[dict[str, Any]]] = None, + ): + """Create new installer information on server. + + This step will create only metadata. Make sure to upload installer + to the server using 'upload_installer' method. + + Runtime python modules are modules that are required to run AYON + desktop application, but are not added to PYTHONPATH for any + subprocess. + + Args: + filename (str): Installer filename. + version (str): Version of installer. + python_version (str): Version of Python. + platform_name (str): Name of platform. + python_modules (dict[str, str]): Python modules that are available + in installer. + runtime_python_modules (dict[str, str]): Runtime python modules + that are available in installer. + checksum (str): Installer file checksum. + checksum_algorithm (str): Type of checksum used to create checksum. + file_size (int): File size. + sources (Optional[list[dict[str, Any]]]): List of sources that + can be used to download file. + + """ + body = { + "filename": filename, + "version": version, + "pythonVersion": python_version, + "platform": platform_name, + "pythonModules": python_modules, + "runtimePythonModules": runtime_python_modules, + "checksum": checksum, + "checksumAlgorithm": checksum_algorithm, + "size": file_size, + } + if sources: + body["sources"] = sources + + response = self.post("desktop/installers", **body) + response.raise_for_status() + + def update_installer(self, filename: str, sources: list[dict[str, Any]]): + """Update installer information on server. + + Args: + filename (str): Installer filename. + sources (list[dict[str, Any]]): List of sources that + can be used to download file. Fully replaces existing sources. + + """ + response = self.patch( + f"desktop/installers/{filename}", + sources=sources + ) + response.raise_for_status() + + def delete_installer(self, filename: str): + """Delete installer from server. + + Args: + filename (str): Installer filename. + + """ + response = self.delete(f"desktop/installers/{filename}") + response.raise_for_status() + + def download_installer( + self, + filename: str, + dst_filepath: str, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None + ): + """Download installer file from server. + + Args: + filename (str): Installer filename. + dst_filepath (str): Destination filepath. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + """ + self.download_file( + f"desktop/installers/{filename}", + dst_filepath, + chunk_size=chunk_size, + progress=progress + ) + + def upload_installer( + self, + src_filepath: str, + dst_filename: str, + progress: Optional[TransferProgress] = None, + ): + """Upload installer file to server. + + Args: + src_filepath (str): Source filepath. + dst_filename (str): Destination filename. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + requests.Response: Response object. + + """ + return self.upload_file( + f"desktop/installers/{dst_filename}", + src_filepath, + progress=progress + ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index bc84fbf6e..7bb26484a 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -13,7 +13,6 @@ import logging import platform import uuid -import warnings from contextlib import contextmanager import typing from typing import Optional, Iterable, Tuple, Generator, Any @@ -67,33 +66,31 @@ fill_own_attribs, ) from ._api_helpers import ( - ActionsAPI, - ActivitiesAPI, + InstallersAPI, + DependencyPackagesAPI, BundlesAddonsAPI, EventsAPI, AttributesAPI, - LinksAPI, - ListsAPI, ProjectsAPI, FoldersAPI, TasksAPI, ProductsAPI, VersionsAPI, - ThumbnailsAPI, - WorkfilesAPI, RepresentationsAPI, + WorkfilesAPI, + ThumbnailsAPI, + ActivitiesAPI, + ActionsAPI, + LinksAPI, + ListsAPI, ) if typing.TYPE_CHECKING: from typing import Union from .typing import ( ServerVersion, - InstallersInfoDict, - DependencyPackagesDict, SecretDict, - AnyEntityDict, - StreamType, ) @@ -210,8 +207,8 @@ def as_user(self, username): class ServerAPI( - ActionsAPI, - ActivitiesAPI, + InstallersAPI, + DependencyPackagesAPI, BundlesAddonsAPI, EventsAPI, AttributesAPI, @@ -222,9 +219,11 @@ class ServerAPI( VersionsAPI, RepresentationsAPI, WorkfilesAPI, + ThumbnailsAPI, + ActivitiesAPI, + ActionsAPI, LinksAPI, ListsAPI, - ThumbnailsAPI, ): """Base handler of connection to server. @@ -1838,381 +1837,6 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: | self.get_attributes_fields_for_type(entity_type) ) - def get_installers( - self, - version: Optional[str] = None, - platform_name: Optional[str] = None, - ) -> "InstallersInfoDict": - """Information about desktop application installers on server. - - Desktop application installers are helpers to download/update AYON - desktop application for artists. - - Args: - version (Optional[str]): Filter installers by version. - platform_name (Optional[str]): Filter installers by platform name. - - Returns: - InstallersInfoDict: Information about installers known for server. - - """ - query = prepare_query_string({ - "version": version or None, - "platform": platform_name or None, - }) - response = self.get(f"desktop/installers{query}") - response.raise_for_status() - return response.data - - def create_installer( - self, - filename: str, - version: str, - python_version: str, - platform_name: str, - python_modules: dict[str, str], - runtime_python_modules: dict[str, str], - checksum: str, - checksum_algorithm: str, - file_size: int, - sources: Optional[list[dict[str, Any]]] = None, - ): - """Create new installer information on server. - - This step will create only metadata. Make sure to upload installer - to the server using 'upload_installer' method. - - Runtime python modules are modules that are required to run AYON - desktop application, but are not added to PYTHONPATH for any - subprocess. - - Args: - filename (str): Installer filename. - version (str): Version of installer. - python_version (str): Version of Python. - platform_name (str): Name of platform. - python_modules (dict[str, str]): Python modules that are available - in installer. - runtime_python_modules (dict[str, str]): Runtime python modules - that are available in installer. - checksum (str): Installer file checksum. - checksum_algorithm (str): Type of checksum used to create checksum. - file_size (int): File size. - sources (Optional[list[dict[str, Any]]]): List of sources that - can be used to download file. - - """ - body = { - "filename": filename, - "version": version, - "pythonVersion": python_version, - "platform": platform_name, - "pythonModules": python_modules, - "runtimePythonModules": runtime_python_modules, - "checksum": checksum, - "checksumAlgorithm": checksum_algorithm, - "size": file_size, - } - if sources: - body["sources"] = sources - - response = self.post("desktop/installers", **body) - response.raise_for_status() - - def update_installer(self, filename: str, sources: list[dict[str, Any]]): - """Update installer information on server. - - Args: - filename (str): Installer filename. - sources (list[dict[str, Any]]): List of sources that - can be used to download file. Fully replaces existing sources. - - """ - response = self.patch( - f"desktop/installers/{filename}", - sources=sources - ) - response.raise_for_status() - - def delete_installer(self, filename: str): - """Delete installer from server. - - Args: - filename (str): Installer filename. - - """ - response = self.delete(f"desktop/installers/{filename}") - response.raise_for_status() - - def download_installer( - self, - filename: str, - dst_filepath: str, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None - ): - """Download installer file from server. - - Args: - filename (str): Installer filename. - dst_filepath (str): Destination filepath. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - """ - self.download_file( - f"desktop/installers/{filename}", - dst_filepath, - chunk_size=chunk_size, - progress=progress - ) - - def upload_installer( - self, - src_filepath: str, - dst_filename: str, - progress: Optional[TransferProgress] = None, - ): - """Upload installer file to server. - - Args: - src_filepath (str): Source filepath. - dst_filename (str): Destination filename. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - requests.Response: Response object. - - """ - return self.upload_file( - f"desktop/installers/{dst_filename}", - src_filepath, - progress=progress - ) - - def _get_dependency_package_route( - self, filename: Optional[str] = None - ) -> str: - endpoint = "desktop/dependencyPackages" - if filename: - return f"{endpoint}/{filename}" - return endpoint - - def get_dependency_packages(self) -> "DependencyPackagesDict": - """Information about dependency packages on server. - - To download dependency package, use 'download_dependency_package' - method and pass in 'filename'. - - Example data structure:: - - { - "packages": [ - { - "filename": str, - "platform": str, - "checksum": str, - "checksumAlgorithm": str, - "size": int, - "sources": list[dict[str, Any]], - "supportedAddons": dict[str, str], - "pythonModules": dict[str, str] - } - ] - } - - Returns: - DependencyPackagesDict: Information about dependency packages - known for server. - - """ - endpoint = self._get_dependency_package_route() - result = self.get(endpoint) - result.raise_for_status() - return result.data - - def create_dependency_package( - self, - filename: str, - python_modules: dict[str, str], - source_addons: dict[str, str], - installer_version: str, - checksum: str, - checksum_algorithm: str, - file_size: int, - sources: Optional[list[dict[str, Any]]] = None, - platform_name: Optional[str] = None, - ): - """Create dependency package on server. - - The package will be created on a server, it is also required to upload - the package archive file (using :meth:`upload_dependency_package`). - - Args: - filename (str): Filename of dependency package. - python_modules (dict[str, str]): Python modules in dependency - package:: - - {"": "", ...} - - source_addons (dict[str, str]): Name of addons for which is - dependency package created:: - - {"": "", ...} - - installer_version (str): Version of installer for which was - package created. - checksum (str): Checksum of archive file where dependencies are. - checksum_algorithm (str): Algorithm used to calculate checksum. - file_size (Optional[int]): Size of file. - sources (Optional[list[dict[str, Any]]]): Information about - sources from where it is possible to get file. - platform_name (Optional[str]): Name of platform for which is - dependency package targeted. Default value is - current platform. - - """ - post_body = { - "filename": filename, - "pythonModules": python_modules, - "sourceAddons": source_addons, - "installerVersion": installer_version, - "checksum": checksum, - "checksumAlgorithm": checksum_algorithm, - "size": file_size, - "platform": platform_name or platform.system().lower(), - } - if sources: - post_body["sources"] = sources - - route = self._get_dependency_package_route() - response = self.post(route, **post_body) - response.raise_for_status() - - def update_dependency_package( - self, filename: str, sources: list[dict[str, Any]] - ): - """Update dependency package metadata on server. - - Args: - filename (str): Filename of dependency package. - sources (list[dict[str, Any]]): Information about - sources from where it is possible to get file. Fully replaces - existing sources. - - """ - response = self.patch( - self._get_dependency_package_route(filename), - sources=sources - ) - response.raise_for_status() - - def delete_dependency_package( - self, filename: str, platform_name: Optional[str] = None - ): - """Remove dependency package for specific platform. - - Args: - filename (str): Filename of dependency package. - platform_name (Optional[str]): Deprecated. - - """ - if platform_name is not None: - warnings.warn( - ( - "Argument 'platform_name' is deprecated in" - " 'delete_dependency_package'. The argument will be" - " removed, please modify your code accordingly." - ), - DeprecationWarning - ) - - route = self._get_dependency_package_route(filename) - response = self.delete(route) - response.raise_for_status("Failed to delete dependency file") - return response.data - - def download_dependency_package( - self, - src_filename: str, - dst_directory: str, - dst_filename: str, - platform_name: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, - ) -> str: - """Download dependency package from server. - - This method requires to have authorized token available. The package - is only downloaded. - - Args: - src_filename (str): Filename of dependency pacakge. - For server version 0.2.0 and lower it is name of package - to download. - dst_directory (str): Where the file should be downloaded. - dst_filename (str): Name of destination filename. - platform_name (Optional[str]): Deprecated. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. - - Returns: - str: Filepath to downloaded file. - - """ - if platform_name is not None: - warnings.warn( - ( - "Argument 'platform_name' is deprecated in" - " 'download_dependency_package'. The argument will be" - " removed, please modify your code accordingly." - ), - DeprecationWarning - ) - route = self._get_dependency_package_route(src_filename) - package_filepath = os.path.join(dst_directory, dst_filename) - self.download_file( - route, - package_filepath, - chunk_size=chunk_size, - progress=progress - ) - return package_filepath - - def upload_dependency_package( - self, - src_filepath: str, - dst_filename: str, - platform_name: Optional[str] = None, - progress: Optional[TransferProgress] = None, - ): - """Upload dependency package to server. - - Args: - src_filepath (str): Path to a package file. - dst_filename (str): Dependency package filename or name of package - for server version 0.2.0 or lower. Must be unique. - platform_name (Optional[str]): Deprecated. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - """ - if platform_name is not None: - warnings.warn( - ( - "Argument 'platform_name' is deprecated in" - " 'upload_dependency_package'. The argument will be" - " removed, please modify your code accordingly." - ), - DeprecationWarning - ) - - route = self._get_dependency_package_route(dst_filename) - self.upload_file(route, src_filepath, progress=progress) - def get_secrets(self) -> list["SecretDict"]: """Get all secrets. From 27bf88ba61cff269cfbdc4ca04e1ded7b88e5dc8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:02:14 +0200 Subject: [PATCH 155/506] moved secrets to separate file --- automated_api.py | 2 + ayon_api/_api_helpers/__init__.py | 2 + ayon_api/_api_helpers/secrets.py | 83 ++++++++++++++++++++++++++++++ ayon_api/server_api.py | 84 ++----------------------------- 4 files changed, 91 insertions(+), 80 deletions(-) create mode 100644 ayon_api/_api_helpers/secrets.py diff --git a/automated_api.py b/automated_api.py index 112e904a1..b32863cd0 100644 --- a/automated_api.py +++ b/automated_api.py @@ -337,6 +337,7 @@ def prepare_api_functions(api_globals): ServerAPI, InstallersAPI, DependencyPackagesAPI, + SecretsAPI, BundlesAddonsAPI, EventsAPI, AttributesAPI, @@ -358,6 +359,7 @@ def prepare_api_functions(api_globals): _items = list(ServerAPI.__dict__.items()) _items.extend(InstallersAPI.__dict__.items()) _items.extend(DependencyPackagesAPI.__dict__.items()) + _items.extend(SecretsAPI.__dict__.items()) _items.extend(ActionsAPI.__dict__.items()) _items.extend(ActivitiesAPI.__dict__.items()) _items.extend(BundlesAddonsAPI.__dict__.items()) diff --git a/ayon_api/_api_helpers/__init__.py b/ayon_api/_api_helpers/__init__.py index 0938461be..6e81904a8 100644 --- a/ayon_api/_api_helpers/__init__.py +++ b/ayon_api/_api_helpers/__init__.py @@ -1,6 +1,7 @@ from .base import BaseServerAPI from .installers import InstallersAPI from .dependency_packages import DependencyPackagesAPI +from .secrets import SecretsAPI from .bundles_addons import BundlesAddonsAPI from .events import EventsAPI from .attributes import AttributesAPI @@ -22,6 +23,7 @@ "BaseServerAPI", "InstallersAPI", "DependencyPackagesAPI", + "SecretsAPI", "BundlesAddonsAPI", "EventsAPI", "AttributesAPI", diff --git a/ayon_api/_api_helpers/secrets.py b/ayon_api/_api_helpers/secrets.py new file mode 100644 index 000000000..f02649fef --- /dev/null +++ b/ayon_api/_api_helpers/secrets.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import typing + +from .base import BaseServerAPI +if typing.TYPE_CHECKING: + from ayon_api.typing import SecretDict + + +class SecretsAPI(BaseServerAPI): + def get_secrets(self) -> list["SecretDict"]: + """Get all secrets. + + Example output:: + + [ + { + "name": "secret_1", + "value": "secret_value_1", + }, + { + "name": "secret_2", + "value": "secret_value_2", + } + ] + + Returns: + list[SecretDict]: List of secret entities. + + """ + response = self.get("secrets") + response.raise_for_status() + return response.data + + def get_secret(self, secret_name: str) -> "SecretDict": + """Get secret by name. + + Example output:: + + { + "name": "secret_name", + "value": "secret_value", + } + + Args: + secret_name (str): Name of secret. + + Returns: + dict[str, str]: Secret entity data. + + """ + response = self.get(f"secrets/{secret_name}") + response.raise_for_status() + return response.data + + def save_secret(self, secret_name: str, secret_value: str): + """Save secret. + + This endpoint can create and update secret. + + Args: + secret_name (str): Name of secret. + secret_value (str): Value of secret. + + """ + response = self.put( + f"secrets/{secret_name}", + name=secret_name, + value=secret_value, + ) + response.raise_for_status() + return response.data + + def delete_secret(self, secret_name: str): + """Delete secret by name. + + Args: + secret_name (str): Name of secret to delete. + + """ + response = self.delete(f"secrets/{secret_name}") + response.raise_for_status() + return response.data diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7bb26484a..f016c0b1f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -15,7 +15,7 @@ import uuid from contextlib import contextmanager import typing -from typing import Optional, Iterable, Tuple, Generator, Any +from typing import Optional, Iterable, Generator, Any import requests @@ -68,6 +68,7 @@ from ._api_helpers import ( InstallersAPI, DependencyPackagesAPI, + SecretsAPI, BundlesAddonsAPI, EventsAPI, AttributesAPI, @@ -89,7 +90,6 @@ from typing import Union from .typing import ( ServerVersion, - SecretDict, AnyEntityDict, StreamType, ) @@ -209,6 +209,7 @@ def as_user(self, username): class ServerAPI( InstallersAPI, DependencyPackagesAPI, + SecretsAPI, BundlesAddonsAPI, EventsAPI, AttributesAPI, @@ -880,8 +881,7 @@ def get_server_version_tuple(self) -> "ServerVersion": This function only returns first three numbers of version. Returns: - Tuple[int, int, int, Union[str, None], Union[str, None]]: Server - version. + ServerVersion: Server version. """ if self._server_version_tuple is None: @@ -1837,82 +1837,6 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: | self.get_attributes_fields_for_type(entity_type) ) - def get_secrets(self) -> list["SecretDict"]: - """Get all secrets. - - Example output:: - - [ - { - "name": "secret_1", - "value": "secret_value_1", - }, - { - "name": "secret_2", - "value": "secret_value_2", - } - ] - - Returns: - list[SecretDict]: List of secret entities. - - """ - response = self.get("secrets") - response.raise_for_status() - return response.data - - def get_secret(self, secret_name: str) -> "SecretDict": - """Get secret by name. - - Example output:: - - { - "name": "secret_name", - "value": "secret_value", - } - - Args: - secret_name (str): Name of secret. - - Returns: - dict[str, str]: Secret entity data. - - """ - response = self.get(f"secrets/{secret_name}") - response.raise_for_status() - return response.data - - def save_secret(self, secret_name: str, secret_value: str): - """Save secret. - - This endpoint can create and update secret. - - Args: - secret_name (str): Name of secret. - secret_value (str): Value of secret. - - """ - response = self.put( - f"secrets/{secret_name}", - name=secret_name, - value=secret_value, - ) - response.raise_for_status() - return response.data - - def delete_secret(self, secret_name: str): - """Delete secret by name. - - Args: - secret_name (str): Name of secret to delete. - - """ - response = self.delete(f"secrets/{secret_name}") - response.raise_for_status() - return response.data - - # Entity getters - def get_rest_entity_by_id( self, project_name: str, From 2957f8c539e8ed1682520eb150291d9a822975c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:04:51 +0200 Subject: [PATCH 156/506] update public api --- ayon_api/__init__.py | 304 +- ayon_api/_api.py | 7941 +++++++++++++++++++++--------------------- 2 files changed, 4122 insertions(+), 4123 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index a12e27624..1d28e6736 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -77,13 +77,9 @@ get_graphql_schema, get_server_schema, get_schemas, - get_attributes_schema, - reset_attributes_schema, - set_attribute_config, - remove_attribute_config, - get_attributes_for_type, - get_attributes_fields_for_type, get_default_fields_for_type, + get_rest_entity_by_id, + send_batch_operations, get_installers, create_installer, update_installer, @@ -96,38 +92,10 @@ delete_dependency_package, download_dependency_package, upload_dependency_package, - get_bundles, - create_bundle, - update_bundle, - check_bundle_compatibility, - delete_bundle, - get_project_anatomy_presets, - get_default_anatomy_preset_name, - get_project_anatomy_preset, - get_built_in_anatomy_preset, - get_build_in_anatomy_preset, - get_project_root_overrides, - get_project_roots_by_site, - get_project_root_overrides_by_site_id, - get_project_roots_for_site, - get_project_roots_by_site_id, - get_project_roots_by_platform, - get_addon_settings_schema, - get_addon_site_settings_schema, - get_addon_studio_settings, - get_addon_project_settings, - get_addon_settings, - get_addon_site_settings, - get_bundle_settings, - get_addons_studio_settings, - get_addons_project_settings, - get_addons_settings, get_secrets, get_secret, save_secret, delete_secret, - get_rest_entity_by_id, - send_batch_operations, get_actions, trigger_action, get_action_config, @@ -140,6 +108,11 @@ update_activity, delete_activity, send_activities_batch_operations, + get_bundles, + create_bundle, + update_bundle, + check_bundle_compatibility, + delete_bundle, get_addon_endpoint, get_addons_info, get_addon_url, @@ -147,43 +120,33 @@ delete_addon_version, upload_addon_zip, download_addon_private_file, + get_addon_settings_schema, + get_addon_site_settings_schema, + get_addon_studio_settings, + get_addon_project_settings, + get_addon_settings, + get_addon_site_settings, + get_bundle_settings, + get_addons_studio_settings, + get_addons_project_settings, + get_addons_settings, get_event, get_events, update_event, dispatch_event, delete_event, enroll_event_job, - get_full_link_type_name, - get_link_types, - get_link_type, - create_link_type, - delete_link_type, - make_sure_link_type_exists, - create_link, - delete_link, - get_entities_links, - get_folders_links, - get_folder_links, - get_tasks_links, - get_task_links, - get_products_links, - get_product_links, - get_versions_links, - get_version_links, - get_representations_links, - get_representation_links, - get_entity_lists, - get_entity_list_rest, - get_entity_list_by_id, - create_entity_list, - update_entity_list, - delete_entity_list, - get_entity_list_attribute_definitions, - set_entity_list_attribute_definitions, - create_entity_list_item, - update_entity_list_items, - update_entity_list_item, - delete_entity_list_item, + get_attributes_schema, + reset_attributes_schema, + set_attribute_config, + remove_attribute_config, + get_attributes_for_type, + get_attributes_fields_for_type, + get_project_anatomy_presets, + get_default_anatomy_preset_name, + get_project_anatomy_preset, + get_built_in_anatomy_preset, + get_build_in_anatomy_preset, get_rest_project, get_rest_projects, get_project_names, @@ -192,6 +155,12 @@ create_project, update_project, delete_project, + get_project_root_overrides, + get_project_roots_by_site, + get_project_root_overrides_by_site_id, + get_project_roots_for_site, + get_project_roots_by_site_id, + get_project_roots_by_platform, get_rest_folder, get_rest_folders, get_folders_hierarchy, @@ -238,17 +207,6 @@ create_version, update_version, delete_version, - get_thumbnail_by_id, - get_thumbnail, - get_folder_thumbnail, - get_task_thumbnail, - get_version_thumbnail, - get_workfile_thumbnail, - create_thumbnail, - update_thumbnail, - get_workfiles_info, - get_workfile_info, - get_workfile_info_by_id, get_rest_representation, get_representations, get_representation_by_id, @@ -261,6 +219,48 @@ create_representation, update_representation, delete_representation, + get_workfiles_info, + get_workfile_info, + get_workfile_info_by_id, + get_full_link_type_name, + get_link_types, + get_link_type, + create_link_type, + delete_link_type, + make_sure_link_type_exists, + create_link, + delete_link, + get_entities_links, + get_folders_links, + get_folder_links, + get_tasks_links, + get_task_links, + get_products_links, + get_product_links, + get_versions_links, + get_version_links, + get_representations_links, + get_representation_links, + get_entity_lists, + get_entity_list_rest, + get_entity_list_by_id, + create_entity_list, + update_entity_list, + delete_entity_list, + get_entity_list_attribute_definitions, + set_entity_list_attribute_definitions, + create_entity_list_item, + update_entity_list_items, + update_entity_list_item, + delete_entity_list_item, + get_thumbnail_by_id, + get_thumbnail, + get_folder_thumbnail, + get_task_thumbnail, + get_version_thumbnail, + get_workfile_thumbnail, + create_thumbnail, + update_thumbnail, ) @@ -341,13 +341,9 @@ "get_graphql_schema", "get_server_schema", "get_schemas", - "get_attributes_schema", - "reset_attributes_schema", - "set_attribute_config", - "remove_attribute_config", - "get_attributes_for_type", - "get_attributes_fields_for_type", "get_default_fields_for_type", + "get_rest_entity_by_id", + "send_batch_operations", "get_installers", "create_installer", "update_installer", @@ -360,38 +356,10 @@ "delete_dependency_package", "download_dependency_package", "upload_dependency_package", - "get_bundles", - "create_bundle", - "update_bundle", - "check_bundle_compatibility", - "delete_bundle", - "get_project_anatomy_presets", - "get_default_anatomy_preset_name", - "get_project_anatomy_preset", - "get_built_in_anatomy_preset", - "get_build_in_anatomy_preset", - "get_project_root_overrides", - "get_project_roots_by_site", - "get_project_root_overrides_by_site_id", - "get_project_roots_for_site", - "get_project_roots_by_site_id", - "get_project_roots_by_platform", - "get_addon_settings_schema", - "get_addon_site_settings_schema", - "get_addon_studio_settings", - "get_addon_project_settings", - "get_addon_settings", - "get_addon_site_settings", - "get_bundle_settings", - "get_addons_studio_settings", - "get_addons_project_settings", - "get_addons_settings", "get_secrets", "get_secret", "save_secret", "delete_secret", - "get_rest_entity_by_id", - "send_batch_operations", "get_actions", "trigger_action", "get_action_config", @@ -404,6 +372,11 @@ "update_activity", "delete_activity", "send_activities_batch_operations", + "get_bundles", + "create_bundle", + "update_bundle", + "check_bundle_compatibility", + "delete_bundle", "get_addon_endpoint", "get_addons_info", "get_addon_url", @@ -411,43 +384,33 @@ "delete_addon_version", "upload_addon_zip", "download_addon_private_file", + "get_addon_settings_schema", + "get_addon_site_settings_schema", + "get_addon_studio_settings", + "get_addon_project_settings", + "get_addon_settings", + "get_addon_site_settings", + "get_bundle_settings", + "get_addons_studio_settings", + "get_addons_project_settings", + "get_addons_settings", "get_event", "get_events", "update_event", "dispatch_event", "delete_event", "enroll_event_job", - "get_full_link_type_name", - "get_link_types", - "get_link_type", - "create_link_type", - "delete_link_type", - "make_sure_link_type_exists", - "create_link", - "delete_link", - "get_entities_links", - "get_folders_links", - "get_folder_links", - "get_tasks_links", - "get_task_links", - "get_products_links", - "get_product_links", - "get_versions_links", - "get_version_links", - "get_representations_links", - "get_representation_links", - "get_entity_lists", - "get_entity_list_rest", - "get_entity_list_by_id", - "create_entity_list", - "update_entity_list", - "delete_entity_list", - "get_entity_list_attribute_definitions", - "set_entity_list_attribute_definitions", - "create_entity_list_item", - "update_entity_list_items", - "update_entity_list_item", - "delete_entity_list_item", + "get_attributes_schema", + "reset_attributes_schema", + "set_attribute_config", + "remove_attribute_config", + "get_attributes_for_type", + "get_attributes_fields_for_type", + "get_project_anatomy_presets", + "get_default_anatomy_preset_name", + "get_project_anatomy_preset", + "get_built_in_anatomy_preset", + "get_build_in_anatomy_preset", "get_rest_project", "get_rest_projects", "get_project_names", @@ -456,6 +419,12 @@ "create_project", "update_project", "delete_project", + "get_project_root_overrides", + "get_project_roots_by_site", + "get_project_root_overrides_by_site_id", + "get_project_roots_for_site", + "get_project_roots_by_site_id", + "get_project_roots_by_platform", "get_rest_folder", "get_rest_folders", "get_folders_hierarchy", @@ -502,17 +471,6 @@ "create_version", "update_version", "delete_version", - "get_thumbnail_by_id", - "get_thumbnail", - "get_folder_thumbnail", - "get_task_thumbnail", - "get_version_thumbnail", - "get_workfile_thumbnail", - "create_thumbnail", - "update_thumbnail", - "get_workfiles_info", - "get_workfile_info", - "get_workfile_info_by_id", "get_rest_representation", "get_representations", "get_representation_by_id", @@ -525,4 +483,46 @@ "create_representation", "update_representation", "delete_representation", + "get_workfiles_info", + "get_workfile_info", + "get_workfile_info_by_id", + "get_full_link_type_name", + "get_link_types", + "get_link_type", + "create_link_type", + "delete_link_type", + "make_sure_link_type_exists", + "create_link", + "delete_link", + "get_entities_links", + "get_folders_links", + "get_folder_links", + "get_tasks_links", + "get_task_links", + "get_products_links", + "get_product_links", + "get_versions_links", + "get_version_links", + "get_representations_links", + "get_representation_links", + "get_entity_lists", + "get_entity_list_rest", + "get_entity_list_by_id", + "create_entity_list", + "update_entity_list", + "delete_entity_list", + "get_entity_list_attribute_definitions", + "set_entity_list_attribute_definitions", + "create_entity_list_item", + "update_entity_list_items", + "update_entity_list_item", + "delete_entity_list_item", + "get_thumbnail_by_id", + "get_thumbnail", + "get_folder_thumbnail", + "get_task_thumbnail", + "get_version_thumbnail", + "get_workfile_thumbnail", + "create_thumbnail", + "update_thumbnail", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index e30357663..475396d29 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -19,13 +19,8 @@ SERVER_URL_ENV_KEY, SERVER_API_ENV_KEY, ) -from .server_api import ( - ServerAPI, - RequestType, - GraphQlResponse, - _PLACEHOLDER, -) from .exceptions import FailedServiceInit +from ._api_helpers.base import _PLACEHOLDER from .utils import ( NOT_SET, SortOrder, @@ -35,6 +30,11 @@ RepresentationParents, RepresentationHierarchy, ) +from .server_api import ( + ServerAPI, + RequestType, + GraphQlResponse, +) if typing.TYPE_CHECKING: from typing import Union @@ -705,8 +705,7 @@ def get_server_version_tuple() -> "ServerVersion": This function only returns first three numbers of version. Returns: - Tuple[int, int, int, Union[str, None], Union[str, None]]: Server - version. + ServerVersion: Server version. """ con = get_server_api_connection() @@ -1162,131 +1161,88 @@ def get_schemas() -> dict[str, Any]: return con.get_schemas() -def get_attributes_schema( - use_cache: bool = True, -) -> "AttributesSchemaDict": - con = get_server_api_connection() - return con.get_attributes_schema( - use_cache=use_cache, - ) - - -def reset_attributes_schema(): - con = get_server_api_connection() - return con.reset_attributes_schema() - - -def set_attribute_config( - attribute_name: str, - data: "AttributeSchemaDataDict", - scope: list["AttributeScope"], - position: Optional[int] = None, - builtin: bool = False, -): - con = get_server_api_connection() - return con.set_attribute_config( - attribute_name=attribute_name, - data=data, - scope=scope, - position=position, - builtin=builtin, - ) - - -def remove_attribute_config( - attribute_name: str, -): - """Remove attribute from server. - - This can't be un-done, please use carefully. - - Args: - attribute_name (str): Name of attribute to remove. - - """ - con = get_server_api_connection() - return con.remove_attribute_config( - attribute_name=attribute_name, - ) - - -def get_attributes_for_type( - entity_type: "AttributeScope", -) -> dict[str, "AttributeSchemaDict"]: - """Get attribute schemas available for an entity type. - - Example:: +def get_default_fields_for_type( + entity_type: str, +) -> set[str]: + """Default fields for entity type. - ``` - # Example attribute schema - { - # Common - "type": "integer", - "title": "Clip Out", - "description": null, - "example": 1, - "default": 1, - # These can be filled based on value of 'type' - "gt": null, - "ge": null, - "lt": null, - "le": null, - "minLength": null, - "maxLength": null, - "minItems": null, - "maxItems": null, - "regex": null, - "enum": null - } - ``` + Returns most of commonly used fields from server. Args: - entity_type (str): Entity type for which should be attributes - received. + entity_type (str): Name of entity type. Returns: - dict[str, dict[str, Any]]: Attribute schemas that are available - for entered entity type. + set[str]: Fields that should be queried from server. """ con = get_server_api_connection() - return con.get_attributes_for_type( + return con.get_default_fields_for_type( entity_type=entity_type, ) -def get_attributes_fields_for_type( - entity_type: "AttributeScope", -) -> set[str]: - """Prepare attribute fields for entity type. +def get_rest_entity_by_id( + project_name: str, + entity_type: str, + entity_id: str, +) -> Optional["AnyEntityDict"]: + """Get entity using REST on a project by its id. + + Args: + project_name (str): Name of project where entity is. + entity_type (Literal["folder", "task", "product", "version"]): The + entity type which should be received. + entity_id (str): Id of entity. Returns: - set[str]: Attributes fields for entity type. + Optional[AnyEntityDict]: Received entity data. """ con = get_server_api_connection() - return con.get_attributes_fields_for_type( + return con.get_rest_entity_by_id( + project_name=project_name, entity_type=entity_type, + entity_id=entity_id, ) -def get_default_fields_for_type( - entity_type: str, -) -> set[str]: - """Default fields for entity type. +def send_batch_operations( + project_name: str, + operations: list[dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> list[dict[str, Any]]: + """Post multiple CRUD operations to server. - Returns most of commonly used fields from 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: - entity_type (str): Name of entity type. + 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: - set[str]: Fields that should be queried from server. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_default_fields_for_type( - entity_type=entity_type, + return con.send_batch_operations( + project_name=project_name, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) @@ -1639,3552 +1595,3883 @@ def upload_dependency_package( ) -def get_bundles() -> "BundlesInfoDict": - """Server bundles with basic information. +def get_secrets() -> list["SecretDict"]: + """Get all secrets. - This is example output:: + Example output:: - { - "bundles": [ - { - "name": "my_bundle", - "createdAt": "2023-06-12T15:37:02.420260", - "installerVersion": "1.0.0", - "addons": { - "core": "1.2.3" - }, - "dependencyPackages": { - "windows": "a_windows_package123.zip", - "linux": "a_linux_package123.zip", - "darwin": "a_mac_package123.zip" - }, - "isProduction": False, - "isStaging": False - } - ], - "productionBundle": "my_bundle", - "stagingBundle": "test_bundle" - } + [ + { + "name": "secret_1", + "value": "secret_value_1", + }, + { + "name": "secret_2", + "value": "secret_value_2", + } + ] Returns: - dict[str, Any]: Server bundles with basic information. + list[SecretDict]: List of secret entities. """ con = get_server_api_connection() - return con.get_bundles() - - -def create_bundle( - name: str, - addon_versions: dict[str, str], - installer_version: str, - dependency_packages: Optional[dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, -): - """Create bundle on server. + return con.get_secrets() - Bundle cannot be changed once is created. Only isProduction, isStaging - and dependency packages can change after creation. In case dev bundle - is created, it is possible to change anything, but it is not possible - to mark bundle as dev and production or staging at the same time. - Development addon config can define custom path to client code. It is - used only for dev bundles. +def get_secret( + secret_name: str, +) -> "SecretDict": + """Get secret by name. - Example of 'dev_addons_config':: + Example output:: - ```json { - "core": { - "enabled": true, - "path": "/path/to/ayon-core/client" - } + "name": "secret_name", + "value": "secret_value", } - ``` Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. + secret_name (str): Name of secret. + + Returns: + dict[str, str]: Secret entity data. """ con = get_server_api_connection() - return con.create_bundle( - name=name, - addon_versions=addon_versions, - installer_version=installer_version, - dependency_packages=dependency_packages, - is_production=is_production, - is_staging=is_staging, - is_dev=is_dev, - dev_active_user=dev_active_user, - dev_addons_config=dev_addons_config, + return con.get_secret( + secret_name=secret_name, ) -def update_bundle( - bundle_name: str, - addon_versions: Optional[dict[str, str]] = None, - installer_version: Optional[str] = None, - dependency_packages: Optional[dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, +def save_secret( + secret_name: str, + secret_value: str, ): - """Update bundle on server. - - Dependency packages can be update only for single platform. Others - will be left untouched. Use 'None' value to unset dependency package - from bundle. - - Args: - bundle_name (str): Name of bundle. - addon_versions (Optional[dict[str, str]]): Addon versions, - possible only for dev bundles. - installer_version (Optional[str]): Installer version, possible - only for dev bundles. - dependency_packages (Optional[dict[str, str]]): Dependency pacakge - names that should be used with the bundle. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only for dev bundles. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only for dev bundles. - - """ - con = get_server_api_connection() - return con.update_bundle( - bundle_name=bundle_name, - addon_versions=addon_versions, - installer_version=installer_version, - dependency_packages=dependency_packages, - is_production=is_production, - is_staging=is_staging, - is_dev=is_dev, - dev_active_user=dev_active_user, - dev_addons_config=dev_addons_config, - ) - - -def check_bundle_compatibility( - name: str, - addon_versions: dict[str, str], - installer_version: str, - dependency_packages: Optional[dict[str, str]] = None, - is_production: Optional[bool] = None, - is_staging: Optional[bool] = None, - is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, - dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, -) -> dict[str, Any]: - """Check bundle compatibility. + """Save secret. - Can be used as per-flight validation before creating bundle. + This endpoint can create and update secret. Args: - name (str): Name of bundle. - addon_versions (dict[str, str]): Addon versions. - installer_version (Union[str, None]): Installer version. - dependency_packages (Optional[dict[str, str]]): Dependency - package names. Keys are platform names and values are name of - packages. - is_production (Optional[bool]): Bundle will be marked as - production. - is_staging (Optional[bool]): Bundle will be marked as staging. - is_dev (Optional[bool]): Bundle will be marked as dev. - dev_active_user (Optional[str]): Username that will be assigned - to dev bundle. Can be used only if 'is_dev' is set to 'True'. - dev_addons_config (Optional[dict[str, Any]]): Configuration for - dev addons. Can be used only if 'is_dev' is set to 'True'. - - Returns: - dict[str, Any]: Server response, with 'success' and 'issues'. + secret_name (str): Name of secret. + secret_value (str): Value of secret. """ con = get_server_api_connection() - return con.check_bundle_compatibility( - name=name, - addon_versions=addon_versions, - installer_version=installer_version, - dependency_packages=dependency_packages, - is_production=is_production, - is_staging=is_staging, - is_dev=is_dev, - dev_active_user=dev_active_user, - dev_addons_config=dev_addons_config, + return con.save_secret( + secret_name=secret_name, + secret_value=secret_value, ) -def delete_bundle( - bundle_name: str, +def delete_secret( + secret_name: str, ): - """Delete bundle from server. + """Delete secret by name. Args: - bundle_name (str): Name of bundle to delete. + secret_name (str): Name of secret to delete. """ con = get_server_api_connection() - return con.delete_bundle( - bundle_name=bundle_name, + return con.delete_secret( + secret_name=secret_name, ) -def get_project_anatomy_presets() -> list["AnatomyPresetDict"]: - """Anatomy presets available on server. - - Content has basic information about presets. Example output:: +def get_actions( + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, + mode: Optional["ActionModeType"] = None, +) -> list["ActionManifestdict"]: + """Get actions for a context. - [ - { - "name": "netflix_VFX", - "primary": false, - "version": "1.0.0" - }, - { - ... - }, - ... - ] + Args: + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + mode (Optional[ActionModeType]): Action modes. Returns: - list[dict[str, str]]: Anatomy presets available on server. + list[ActionManifestdict]: list of action manifests. """ con = get_server_api_connection() - return con.get_project_anatomy_presets() - + return con.get_actions( + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + mode=mode, + ) -def get_default_anatomy_preset_name() -> str: - """Name of default anatomy preset. - Primary preset is used as default preset. But when primary preset is - not set a built-in is used instead. Built-in preset is named '_'. +def trigger_action( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionTriggerResponse": + """Trigger action. - Returns: - str: Name of preset that can be used by - 'get_project_anatomy_preset'. + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. """ con = get_server_api_connection() - return con.get_default_anatomy_preset_name() - + return con.trigger_action( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) -def get_project_anatomy_preset( - preset_name: Optional[str] = None, -) -> "AnatomyPresetDict": - """Anatomy preset values by name. - Get anatomy preset values by preset name. Primary preset is returned - if preset name is set to 'None'. +def get_action_config( + identifier: str, + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Get action configuration. Args: - preset_name (Optional[str]): Preset name. + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. Returns: - AnatomyPresetDict: Anatomy preset values. + ActionConfigResponse: Action configuration data. """ con = get_server_api_connection() - return con.get_project_anatomy_preset( - preset_name=preset_name, + return con.get_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, ) -def get_built_in_anatomy_preset() -> "AnatomyPresetDict": - """Get built-in anatomy preset. +def set_action_config( + identifier: str, + addon_name: str, + addon_version: str, + value: dict[str, Any], + project_name: Optional[str] = None, + entity_type: Optional["ActionEntityTypes"] = None, + entity_ids: Optional[list[str]] = None, + entity_subtypes: Optional[list[str]] = None, + form_data: Optional[dict[str, Any]] = None, + *, + variant: Optional[str] = None, +) -> "ActionConfigResponse": + """Set action configuration. - Returns: - AnatomyPresetDict: Built-in anatomy preset. - - """ - con = get_server_api_connection() - return con.get_built_in_anatomy_preset() + Args: + identifier (str): Identifier of the action. + addon_name (str): Name of the addon. + addon_version (str): Version of the addon. + value (Optional[dict[str, Any]]): Value of the action + configuration. + project_name (Optional[str]): Name of the project. None for global + actions. + entity_type (Optional[ActionEntityTypes]): Entity type where the + action is triggered. None for global actions. + entity_ids (Optional[list[str]]): list of entity ids where the + action is triggered. None for global actions. + entity_subtypes (Optional[list[str]]): list of entity subtypes + folder types for folder ids, task types for tasks ids. + form_data (Optional[dict[str, Any]]): Form data of the action. + variant (Optional[str]): Settings variant. + Returns: + ActionConfigResponse: New action configuration data. -def get_build_in_anatomy_preset() -> "AnatomyPresetDict": + """ con = get_server_api_connection() - return con.get_build_in_anatomy_preset() - - -def get_project_root_overrides( - project_name: str, -) -> dict[str, dict[str, str]]: - """Root overrides per site name. + return con.set_action_config( + identifier=identifier, + addon_name=addon_name, + addon_version=addon_version, + value=value, + project_name=project_name, + entity_type=entity_type, + entity_ids=entity_ids, + entity_subtypes=entity_subtypes, + form_data=form_data, + variant=variant, + ) - Method is based on logged user and can't be received for any other - user on server. - Output will contain only roots per site id used by logged user. +def take_action( + action_token: str, +) -> "ActionTakeResponse": + """Take action metadata using an action token. Args: - project_name (str): Name of project. + action_token (str): AYON launcher action token. Returns: - dict[str, dict[str, str]]: Root values by root name by site id. + ActionTakeResponse: Action metadata describing how to launch + action. """ con = get_server_api_connection() - return con.get_project_root_overrides( - project_name=project_name, + return con.take_action( + action_token=action_token, ) -def get_project_roots_by_site( - project_name: str, -) -> dict[str, dict[str, str]]: - """Root overrides per site name. - - Method is based on logged user and can't be received for any other - user on server. - - Output will contain only roots per site id used by logged user. - - Deprecated: - Use 'get_project_root_overrides' instead. Function - deprecated since 1.0.6 +def abort_action( + action_token: str, + message: Optional[str] = None, +) -> None: + """Abort action using an action token. Args: - project_name (str): Name of project. - - Returns: - dict[str, dict[str, str]]: Root values by root name by site id. + action_token (str): AYON launcher action token. + message (Optional[str]): Message to display in the UI. """ con = get_server_api_connection() - return con.get_project_roots_by_site( - project_name=project_name, + return con.abort_action( + action_token=action_token, + message=message, ) -def get_project_root_overrides_by_site_id( +def get_activities( project_name: str, - site_id: Optional[str] = None, -) -> dict[str, str]: - """Root overrides for site. - - If site id is not passed a site set in current api object is used - instead. + 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["ActivityReferenceType"]] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, +) -> Generator[dict[str, Any], None, None]: + """Get activities from server with filtering options. Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. + 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[ActivityReferenceType]]): + Reference types filter. Defaults to `['origin']`. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. + limit (Optional[int]): Limit number of activities to be fetched. + order (Optional[SortOrder]): Order activities in ascending + or descending order. It is recommended to set 'limit' + when used descending. Returns: - dict[str, str]: Root values by root name or None if - site does not have overrides. + Generator[dict[str, Any]]: Available activities matching filters. """ con = get_server_api_connection() - return con.get_project_root_overrides_by_site_id( + return con.get_activities( project_name=project_name, - site_id=site_id, + activity_ids=activity_ids, + activity_types=activity_types, + entity_ids=entity_ids, + entity_names=entity_names, + entity_type=entity_type, + changed_after=changed_after, + changed_before=changed_before, + reference_types=reference_types, + fields=fields, + limit=limit, + order=order, ) -def get_project_roots_for_site( +def get_activity_by_id( project_name: str, - site_id: Optional[str] = None, -) -> dict[str, str]: - """Root overrides for site. - - If site id is not passed a site set in current api object is used - instead. + activity_id: str, + reference_types: Optional[Iterable["ActivityReferenceType"]] = None, + fields: Optional[Iterable[str]] = None, +) -> Optional[dict[str, Any]]: + """Get activity by id. - Deprecated: - Use 'get_project_root_overrides_by_site_id' instead. Function - deprecated since 1.0.6 Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - site overrides. + project_name (str): Project on which activity happened. + activity_id (str): Activity id. + reference_types: Optional[Iterable[ActivityReferenceType]]: Filter + by reference types. + fields (Optional[Iterable[str]]): Fields that should be received + for each activity. Returns: - dict[str, str]: Root values by root name, root name is not - available if it does not have overrides. + Optional[dict[str, Any]]: Activity data or None if activity is not + found. """ con = get_server_api_connection() - return con.get_project_roots_for_site( + return con.get_activity_by_id( project_name=project_name, - site_id=site_id, + activity_id=activity_id, + reference_types=reference_types, + fields=fields, ) -def get_project_roots_by_site_id( +def create_activity( project_name: str, - site_id: Optional[str] = None, -) -> dict[str, str]: - """Root values for a site. - - If site id is not passed a site set in current api object is used - instead. If site id is not available, default roots are returned - for current platform. + 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, +) -> str: + """Create activity on a project. Args: - project_name (str): Name of project. - site_id (Optional[str]): Site id for which want to receive - root values. + 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]: Root values. + str: Activity id. """ con = get_server_api_connection() - return con.get_project_roots_by_site_id( + return con.create_activity( project_name=project_name, - site_id=site_id, + entity_id=entity_id, + entity_type=entity_type, + activity_type=activity_type, + activity_id=activity_id, + body=body, + file_ids=file_ids, + timestamp=timestamp, + data=data, ) -def get_project_roots_by_platform( +def update_activity( project_name: str, - platform_name: Optional[str] = None, -) -> dict[str, str]: - """Root values for a site. - - If platform name is not passed current platform name is used instead. - - This function does return root values without site overrides. It is - possible to use the function to receive default root values. + activity_id: str, + body: Optional[str] = None, + file_ids: Optional[list[str]] = None, + append_file_ids: Optional[bool] = False, + data: Optional[dict[str, Any]] = None, +): + """Update activity by id. Args: - project_name (str): Name of project. - platform_name (Optional[Literal["windows", "linux", "darwin"]]): - Platform name for which want to receive root values. Current - platform name is used if not passed. - - Returns: - dict[str, str]: Root values. + 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. + 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() - return con.get_project_roots_by_platform( + return con.update_activity( project_name=project_name, - platform_name=platform_name, + activity_id=activity_id, + body=body, + file_ids=file_ids, + append_file_ids=append_file_ids, + data=data, ) -def get_addon_settings_schema( - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, -) -> dict[str, Any]: - """Sudio/Project settings schema of an addon. - - Project schema may look differently as some enums are based on project - values. +def delete_activity( + project_name: str, + activity_id: str, +): + """Delete activity by id. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (Optional[str]): Schema for specific project or - default studio schemas. - - Returns: - dict[str, Any]: Schema of studio/project settings. + project_name (str): Project on which activity happened. + activity_id (str): Activity id to remove. """ con = get_server_api_connection() - return con.get_addon_settings_schema( - addon_name=addon_name, - addon_version=addon_version, + return con.delete_activity( project_name=project_name, + activity_id=activity_id, ) -def get_addon_site_settings_schema( - addon_name: str, - addon_version: str, -) -> dict[str, Any]: - """Site settings schema of an addon. +def send_activities_batch_operations( + project_name: str, + operations: list[dict[str, Any]], + can_fail: bool = False, + raise_on_fail: bool = True, +) -> list[dict[str, Any]]: + """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: - addon_name (str): Name of addon. - addon_version (str): Version of addon. + 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: - dict[str, Any]: Schema of site settings. + list[dict[str, Any]]: Operations result with process details. """ con = get_server_api_connection() - return con.get_addon_site_settings_schema( - addon_name=addon_name, - addon_version=addon_version, + return con.send_activities_batch_operations( + project_name=project_name, + operations=operations, + can_fail=can_fail, + raise_on_fail=raise_on_fail, ) -def get_addon_studio_settings( - addon_name: str, - addon_version: str, - variant: Optional[str] = None, -) -> dict[str, Any]: - """Addon studio settings. +def get_bundles() -> "BundlesInfoDict": + """Server bundles with basic information. - Receive studio settings for specific version of an addon. + This is example output:: - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. + { + "bundles": [ + { + "name": "my_bundle", + "createdAt": "2023-06-12T15:37:02.420260", + "installerVersion": "1.0.0", + "addons": { + "core": "1.2.3" + }, + "dependencyPackages": { + "windows": "a_windows_package123.zip", + "linux": "a_linux_package123.zip", + "darwin": "a_mac_package123.zip" + }, + "isProduction": False, + "isStaging": False + } + ], + "productionBundle": "my_bundle", + "stagingBundle": "test_bundle" + } Returns: - dict[str, Any]: Addon settings. + dict[str, Any]: Server bundles with basic information. """ con = get_server_api_connection() - return con.get_addon_studio_settings( - addon_name=addon_name, - addon_version=addon_version, - variant=variant, - ) + return con.get_bundles() -def get_addon_project_settings( - addon_name: str, - addon_version: str, - project_name: str, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, -) -> dict[str, Any]: - """Addon project settings. +def create_bundle( + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, +): + """Create bundle on server. - Receive project settings for specific version of an addon. The settings - may be with site overrides when enabled. + Bundle cannot be changed once is created. Only isProduction, isStaging + and dependency packages can change after creation. In case dev bundle + is created, it is possible to change anything, but it is not possible + to mark bundle as dev and production or staging at the same time. - Site id is filled with current connection site id if not passed. To - make sure any site id is used set 'use_site' to 'False'. + Development addon config can define custom path to client code. It is + used only for dev bundles. - Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (str): Name of project for which the settings are - received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Name of site which is used for site - overrides. Is filled with connection 'site_id' attribute - if not passed. - use_site (Optional[bool]): To force disable option of using site - overrides set to 'False'. In that case won't be applied - any site overrides. + Example of 'dev_addons_config':: - Returns: - dict[str, Any]: Addon settings. + ```json + { + "core": { + "enabled": true, + "path": "/path/to/ayon-core/client" + } + } + ``` + + Args: + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. """ con = get_server_api_connection() - return con.get_addon_project_settings( - addon_name=addon_name, - addon_version=addon_version, - project_name=project_name, - variant=variant, - site_id=site_id, - use_site=use_site, + return con.create_bundle( + name=name, + addon_versions=addon_versions, + installer_version=installer_version, + dependency_packages=dependency_packages, + is_production=is_production, + is_staging=is_staging, + is_dev=is_dev, + dev_active_user=dev_active_user, + dev_addons_config=dev_addons_config, ) -def get_addon_settings( - addon_name: str, - addon_version: str, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, -) -> dict[str, Any]: - """Receive addon settings. +def update_bundle( + bundle_name: str, + addon_versions: Optional[dict[str, str]] = None, + installer_version: Optional[str] = None, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, +): + """Update bundle on server. - Receive addon settings based on project name value. Some arguments may - be ignored if 'project_name' is set to 'None'. + Dependency packages can be update only for single platform. Others + will be left untouched. Use 'None' value to unset dependency package + from bundle. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - project_name (Optional[str]): Name of project for which the - settings are received. A studio settings values are received - if is 'None'. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Name of site which is used for site - overrides. Is filled with connection 'site_id' attribute - if not passed. - use_site (Optional[bool]): To force disable option of using - site overrides set to 'False'. In that case won't be applied - any site overrides. - - Returns: - dict[str, Any]: Addon settings. - - """ - con = get_server_api_connection() - return con.get_addon_settings( - addon_name=addon_name, - addon_version=addon_version, - project_name=project_name, - variant=variant, - site_id=site_id, - use_site=use_site, + bundle_name (str): Name of bundle. + addon_versions (Optional[dict[str, str]]): Addon versions, + possible only for dev bundles. + installer_version (Optional[str]): Installer version, possible + only for dev bundles. + dependency_packages (Optional[dict[str, str]]): Dependency pacakge + names that should be used with the bundle. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only for dev bundles. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only for dev bundles. + + """ + con = get_server_api_connection() + return con.update_bundle( + bundle_name=bundle_name, + addon_versions=addon_versions, + installer_version=installer_version, + dependency_packages=dependency_packages, + is_production=is_production, + is_staging=is_staging, + is_dev=is_dev, + dev_active_user=dev_active_user, + dev_addons_config=dev_addons_config, ) -def get_addon_site_settings( - addon_name: str, - addon_version: str, - site_id: Optional[str] = None, +def check_bundle_compatibility( + name: str, + addon_versions: dict[str, str], + installer_version: str, + dependency_packages: Optional[dict[str, str]] = None, + is_production: Optional[bool] = None, + is_staging: Optional[bool] = None, + is_dev: Optional[bool] = None, + dev_active_user: Optional[str] = None, + dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, ) -> dict[str, Any]: - """Site settings of an addon. + """Check bundle compatibility. - If site id is not available an empty dictionary is returned. + Can be used as per-flight validation before creating bundle. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - site_id (Optional[str]): Name of site for which should be settings - returned. using 'site_id' attribute if not passed. + name (str): Name of bundle. + addon_versions (dict[str, str]): Addon versions. + installer_version (Union[str, None]): Installer version. + dependency_packages (Optional[dict[str, str]]): Dependency + package names. Keys are platform names and values are name of + packages. + is_production (Optional[bool]): Bundle will be marked as + production. + is_staging (Optional[bool]): Bundle will be marked as staging. + is_dev (Optional[bool]): Bundle will be marked as dev. + dev_active_user (Optional[str]): Username that will be assigned + to dev bundle. Can be used only if 'is_dev' is set to 'True'. + dev_addons_config (Optional[dict[str, Any]]): Configuration for + dev addons. Can be used only if 'is_dev' is set to 'True'. Returns: - dict[str, Any]: Site settings. + dict[str, Any]: Server response, with 'success' and 'issues'. """ con = get_server_api_connection() - return con.get_addon_site_settings( - addon_name=addon_name, - addon_version=addon_version, - site_id=site_id, + return con.check_bundle_compatibility( + name=name, + addon_versions=addon_versions, + installer_version=installer_version, + dependency_packages=dependency_packages, + is_production=is_production, + is_staging=is_staging, + is_dev=is_dev, + dev_active_user=dev_active_user, + dev_addons_config=dev_addons_config, ) -def get_bundle_settings( - bundle_name: Optional[str] = None, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, -) -> dict[str, Any]: - """Get complete set of settings for given data. - - If project is not passed then studio settings are returned. If variant - is not passed 'default_settings_variant' is used. If bundle name is - not passed then current production/staging bundle is used, based on - variant value. - - Output contains addon settings and site settings in single dictionary. - - Todos: - - test how it behaves if there is not any bundle. - - test how it behaves if there is not any production/staging - bundle. - - Example output:: - - { - "addons": [ - { - "name": "addon-name", - "version": "addon-version", - "settings": {...}, - "siteSettings": {...} - } - ] - } +def delete_bundle( + bundle_name: str, +): + """Delete bundle from server. - Returns: - dict[str, Any]: All settings for single bundle. + Args: + bundle_name (str): Name of bundle to delete. """ con = get_server_api_connection() - return con.get_bundle_settings( + return con.delete_bundle( bundle_name=bundle_name, - project_name=project_name, - variant=variant, - site_id=site_id, - use_site=use_site, ) -def get_addons_studio_settings( - bundle_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, -) -> dict[str, Any]: - """All addons settings in one bulk. +def get_addon_endpoint( + addon_name: str, + addon_version: str, + *subpaths, +) -> str: + """Calculate endpoint to addon route. - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'addons/example/1.0.0/private/my.zip' Args: - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Site id for which want to receive - site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. - only_values (Optional[bool]): Output will contain only settings - values without metadata about addons. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. Returns: - dict[str, Any]: Settings of all addons on server. + str: Final url. """ con = get_server_api_connection() - return con.get_addons_studio_settings( - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values, + return con.get_addon_endpoint( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, ) -def get_addons_project_settings( - project_name: str, - bundle_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, -) -> dict[str, Any]: - """Project settings of all addons. +def get_addons_info( + details: bool = True, +) -> "AddonsInfoDict": + """Get information about addons available on server. - Server returns information about used addon versions, so full output - looks like: + Args: + details (Optional[bool]): Detailed data with information how + to get client code. - ```json - { - "settings": {...}, - "addons": {...} - } - ``` + """ + con = get_server_api_connection() + return con.get_addons_info( + details=details, + ) - The output can be limited to only values. To do so is 'only_values' - argument which is by default set to 'True'. In that case output - contains only value of 'settings' key. - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. +def get_addon_url( + addon_name: str, + addon_version: str, + *subpaths, + use_rest: bool = True, +) -> str: + """Calculate url to addon route. + + Examples: + >>> from ayon_api import ServerAPI + >>> api = ServerAPI("https://your.url.com") + >>> api.get_addon_url( + ... "example", "1.0.0", "private", "my.zip") + 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' Args: - project_name (str): Name of project for which are settings - received. - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Site id for which want to receive - site overrides. - use_site (bool): To force disable option of using site overrides - set to 'False'. In that case won't be applied any site - overrides. - only_values (Optional[bool]): Output will contain only settings - values without metadata about addons. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + *subpaths (str): Any amount of subpaths that are added to + addon url. + use_rest (Optional[bool]): Use rest endpoint. Returns: - dict[str, Any]: Settings of all addons on server for passed - project. + str: Final url. """ con = get_server_api_connection() - return con.get_addons_project_settings( - project_name=project_name, - bundle_name=bundle_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values, + return con.get_addon_url( + addon_name=addon_name, + addon_version=addon_version, + *subpaths, + use_rest=use_rest, ) -def get_addons_settings( - bundle_name: Optional[str] = None, - project_name: Optional[str] = None, - variant: Optional[str] = None, - site_id: Optional[str] = None, - use_site: bool = True, - only_values: bool = True, -) -> dict[str, Any]: - """Universal function to receive all addon settings. - - Based on 'project_name' will receive studio settings or project - settings. In case project is not passed is 'site_id' ignored. +def delete_addon( + addon_name: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon from server. - Warnings: - Behavior of this function changed with AYON server version 0.3.0. - Structure of output from server changed. If using - 'only_values=True' then output should be same as before. + Delete all versions of addon from server. Args: - bundle_name (Optional[str]): Name of bundle for which should be - settings received. - project_name (Optional[str]): Name of project for which should be - settings received. - variant (Optional[Literal['production', 'staging']]): Name of - settings variant. Used 'default_settings_variant' by default. - site_id (Optional[str]): Id of site for which want to receive - site overrides. - use_site (Optional[bool]): To force disable option of using site - overrides set to 'False'. In that case won't be applied - any site overrides. - only_values (Optional[bool]): Only settings values will be - returned. By default, is set to 'True'. + addon_name (str): Addon name. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_addons_settings( - bundle_name=bundle_name, - project_name=project_name, - variant=variant, - site_id=site_id, - use_site=use_site, - only_values=only_values, + return con.delete_addon( + addon_name=addon_name, + purge=purge, ) -def get_secrets() -> list["SecretDict"]: - """Get all secrets. - - Example output:: +def delete_addon_version( + addon_name: str, + addon_version: str, + purge: Optional[bool] = None, +) -> None: + """Delete addon version from server. - [ - { - "name": "secret_1", - "value": "secret_value_1", - }, - { - "name": "secret_2", - "value": "secret_value_2", - } - ] + Delete all versions of addon from server. - Returns: - list[SecretDict]: List of secret entities. + Args: + addon_name (str): Addon name. + addon_version (str): Addon version. + purge (Optional[bool]): Purge all data related to the addon. """ con = get_server_api_connection() - return con.get_secrets() + return con.delete_addon_version( + addon_name=addon_name, + addon_version=addon_version, + purge=purge, + ) -def get_secret( - secret_name: str, -) -> "SecretDict": - """Get secret by name. +def upload_addon_zip( + src_filepath: str, + progress: Optional[TransferProgress] = None, +): + """Upload addon zip file to server. + + File is validated on server. If it is valid, it is installed. It will + create an event job which can be tracked (tracking part is not + implemented yet). Example output:: - { - "name": "secret_name", - "value": "secret_value", - } + {'eventId': 'a1bfbdee27c611eea7580242ac120003'} Args: - secret_name (str): Name of secret. + src_filepath (str): Path to a zip file. + progress (Optional[TransferProgress]): Object to keep track about + upload state. Returns: - dict[str, str]: Secret entity data. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.get_secret( - secret_name=secret_name, + return con.upload_addon_zip( + src_filepath=src_filepath, + progress=progress, ) -def save_secret( - secret_name: str, - secret_value: str, -): - """Save secret. +def download_addon_private_file( + addon_name: str, + addon_version: str, + filename: str, + destination_dir: str, + destination_filename: Optional[str] = None, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> str: + """Download a file from addon private files. - This endpoint can create and update secret. + This method requires to have authorized token available. Private files + are not under '/api' restpoint. Args: - secret_name (str): Name of secret. - secret_value (str): Value of secret. + addon_name (str): Addon name. + addon_version (str): Addon version. + filename (str): Filename in private folder on server. + destination_dir (str): Where the file should be downloaded. + destination_filename (Optional[str]): Name of destination + filename. Source filename is used if not passed. + chunk_size (Optional[int]): Download chunk size. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + str: Filepath to downloaded file. """ con = get_server_api_connection() - return con.save_secret( - secret_name=secret_name, - secret_value=secret_value, + return con.download_addon_private_file( + addon_name=addon_name, + addon_version=addon_version, + filename=filename, + destination_dir=destination_dir, + destination_filename=destination_filename, + chunk_size=chunk_size, + progress=progress, ) -def delete_secret( - secret_name: str, -): - """Delete secret by name. +def get_addon_settings_schema( + addon_name: str, + addon_version: str, + project_name: Optional[str] = None, +) -> dict[str, Any]: + """Sudio/Project settings schema of an addon. + + Project schema may look differently as some enums are based on project + values. Args: - secret_name (str): Name of secret to delete. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Optional[str]): Schema for specific project or + default studio schemas. + + Returns: + dict[str, Any]: Schema of studio/project settings. """ con = get_server_api_connection() - return con.delete_secret( - secret_name=secret_name, + return con.get_addon_settings_schema( + addon_name=addon_name, + addon_version=addon_version, + project_name=project_name, ) -def get_rest_entity_by_id( - project_name: str, - entity_type: str, - entity_id: str, -) -> Optional["AnyEntityDict"]: - """Get entity using REST on a project by its id. +def get_addon_site_settings_schema( + addon_name: str, + addon_version: str, +) -> dict[str, Any]: + """Site settings schema of an addon. Args: - project_name (str): Name of project where entity is. - entity_type (Literal["folder", "task", "product", "version"]): The - entity type which should be received. - entity_id (str): Id of entity. + addon_name (str): Name of addon. + addon_version (str): Version of addon. Returns: - Optional[AnyEntityDict]: Received entity data. + dict[str, Any]: Schema of site settings. """ con = get_server_api_connection() - return con.get_rest_entity_by_id( - project_name=project_name, - entity_type=entity_type, - entity_id=entity_id, + return con.get_addon_site_settings_schema( + addon_name=addon_name, + addon_version=addon_version, ) -def send_batch_operations( - project_name: str, - operations: list[dict[str, Any]], - can_fail: bool = False, - raise_on_fail: bool = True, -) -> list[dict[str, Any]]: - """Post multiple CRUD operations to server. +def get_addon_studio_settings( + addon_name: str, + addon_version: str, + variant: Optional[str] = None, +) -> dict[str, Any]: + """Addon studio settings. - 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. + Receive studio settings for specific version of an addon. 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. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. Returns: - list[dict[str, Any]]: Operations result with process details. + dict[str, Any]: Addon settings. """ con = get_server_api_connection() - return con.send_batch_operations( - project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + return con.get_addon_studio_settings( + addon_name=addon_name, + addon_version=addon_version, + variant=variant, ) -def get_actions( - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, +def get_addon_project_settings( + addon_name: str, + addon_version: str, + project_name: str, variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> List["ActionManifestDict"]: - """Get actions for a context. + site_id: Optional[str] = None, + use_site: bool = True, +) -> dict[str, Any]: + """Addon project settings. + + Receive project settings for specific version of an addon. The settings + may be with site overrides when enabled. + + Site id is filled with current connection site id if not passed. To + make sure any site id is used set 'use_site' to 'False'. Args: - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. - mode (Optional[ActionModeType]): Action modes. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (str): Name of project for which the settings are + received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. Returns: - List[ActionManifestDict]: List of action manifests. + dict[str, Any]: Addon settings. """ con = get_server_api_connection() - return con.get_actions( + return con.get_addon_project_settings( + addon_name=addon_name, + addon_version=addon_version, project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, variant=variant, - mode=mode, + site_id=site_id, + use_site=use_site, ) -def trigger_action( - identifier: str, +def get_addon_settings( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, variant: Optional[str] = None, -) -> "ActionTriggerResponse": - """Trigger action. + site_id: Optional[str] = None, + use_site: bool = True, +) -> dict[str, Any]: + """Receive addon settings. + + Receive addon settings based on project name value. Some arguments may + be ignored if 'project_name' is set to 'None'. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + project_name (Optional[str]): Name of project for which the + settings are received. A studio settings values are received + if is 'None'. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Name of site which is used for site + overrides. Is filled with connection 'site_id' attribute + if not passed. + use_site (Optional[bool]): To force disable option of using + site overrides set to 'False'. In that case won't be applied + any site overrides. + + Returns: + dict[str, Any]: Addon settings. """ con = get_server_api_connection() - return con.trigger_action( - identifier=identifier, + return con.get_addon_settings( addon_name=addon_name, addon_version=addon_version, project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, variant=variant, + site_id=site_id, + use_site=use_site, ) -def get_action_config( - identifier: str, +def get_addon_site_settings( addon_name: str, addon_version: str, - project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, - variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Get action configuration. + site_id: Optional[str] = None, +) -> dict[str, Any]: + """Site settings of an addon. + + If site id is not available an empty dictionary is returned. Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + addon_name (str): Name of addon. + addon_version (str): Version of addon. + site_id (Optional[str]): Name of site for which should be settings + returned. using 'site_id' attribute if not passed. Returns: - ActionConfigResponse: Action configuration data. + dict[str, Any]: Site settings. """ con = get_server_api_connection() - return con.get_action_config( - identifier=identifier, + return con.get_addon_site_settings( addon_name=addon_name, addon_version=addon_version, - project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, - variant=variant, + site_id=site_id, ) -def set_action_config( - identifier: str, - addon_name: str, - addon_version: str, - value: Dict[str, Any], +def get_bundle_settings( + bundle_name: Optional[str] = None, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, - entity_ids: Optional[List[str]] = None, - entity_subtypes: Optional[List[str]] = None, - form_data: Optional[Dict[str, Any]] = None, - *, variant: Optional[str] = None, -) -> "ActionConfigResponse": - """Set action configuration. + site_id: Optional[str] = None, + use_site: bool = True, +) -> dict[str, Any]: + """Get complete set of settings for given data. - Args: - identifier (str): Identifier of the action. - addon_name (str): Name of the addon. - addon_version (str): Version of the addon. - value (Optional[Dict[str, Any]]): Value of the action - configuration. - project_name (Optional[str]): Name of the project. None for global - actions. - entity_type (Optional[ActionEntityTypes]): Entity type where the - action is triggered. None for global actions. - entity_ids (Optional[List[str]]): List of entity ids where the - action is triggered. None for global actions. - entity_subtypes (Optional[List[str]]): List of entity subtypes - folder types for folder ids, task types for tasks ids. - form_data (Optional[Dict[str, Any]]): Form data of the action. - variant (Optional[str]): Settings variant. + If project is not passed then studio settings are returned. If variant + is not passed 'default_settings_variant' is used. If bundle name is + not passed then current production/staging bundle is used, based on + variant value. + + Output contains addon settings and site settings in single dictionary. + + Todos: + - test how it behaves if there is not any bundle. + - test how it behaves if there is not any production/staging + bundle. + + Example output:: + + { + "addons": [ + { + "name": "addon-name", + "version": "addon-version", + "settings": {...}, + "siteSettings": {...} + } + ] + } Returns: - ActionConfigResponse: New action configuration data. + dict[str, Any]: All settings for single bundle. """ con = get_server_api_connection() - return con.set_action_config( - identifier=identifier, - addon_name=addon_name, - addon_version=addon_version, - value=value, + return con.get_bundle_settings( + bundle_name=bundle_name, project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - entity_subtypes=entity_subtypes, - form_data=form_data, variant=variant, + site_id=site_id, + use_site=use_site, ) -def take_action( - action_token: str, -) -> "ActionTakeResponse": - """Take action metadata using an action token. +def get_addons_studio_settings( + bundle_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, +) -> dict[str, Any]: + """All addons settings in one bulk. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. Args: - action_token (str): AYON launcher action token. + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. Returns: - ActionTakeResponse: Action metadata describing how to launch - action. + dict[str, Any]: Settings of all addons on server. """ con = get_server_api_connection() - return con.take_action( - action_token=action_token, + return con.get_addons_studio_settings( + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values, ) -def abort_action( - action_token: str, - message: Optional[str] = None, -) -> None: - """Abort action using an action token. - - Args: - action_token (str): AYON launcher action token. - message (Optional[str]): Message to display in the UI. +def get_addons_project_settings( + project_name: str, + bundle_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, +) -> dict[str, Any]: + """Project settings of all addons. - """ - con = get_server_api_connection() - return con.abort_action( - action_token=action_token, - message=message, - ) + Server returns information about used addon versions, so full output + looks like: + ```json + { + "settings": {...}, + "addons": {...} + } + ``` -def get_activities( - project_name: str, - 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["ActivityReferenceType"]] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, -) -> Generator[dict[str, Any], None, None]: - """Get activities from server with filtering options. + The output can be limited to only values. To do so is 'only_values' + argument which is by default set to 'True'. In that case output + contains only value of 'settings' key. + + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. 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[ActivityReferenceType]]): - Reference types filter. Defaults to `['origin']`. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. - limit (Optional[int]): Limit number of activities to be fetched. - order (Optional[SortOrder]): Order activities in ascending - or descending order. It is recommended to set 'limit' - when used descending. + project_name (str): Name of project for which are settings + received. + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Site id for which want to receive + site overrides. + use_site (bool): To force disable option of using site overrides + set to 'False'. In that case won't be applied any site + overrides. + only_values (Optional[bool]): Output will contain only settings + values without metadata about addons. Returns: - Generator[dict[str, Any]]: Available activities matching filters. + dict[str, Any]: Settings of all addons on server for passed + project. """ con = get_server_api_connection() - return con.get_activities( + return con.get_addons_project_settings( project_name=project_name, - activity_ids=activity_ids, - activity_types=activity_types, - entity_ids=entity_ids, - entity_names=entity_names, - entity_type=entity_type, - changed_after=changed_after, - changed_before=changed_before, - reference_types=reference_types, - fields=fields, - limit=limit, - order=order, + bundle_name=bundle_name, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values, ) -def get_activity_by_id( - 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. +def get_addons_settings( + bundle_name: Optional[str] = None, + project_name: Optional[str] = None, + variant: Optional[str] = None, + site_id: Optional[str] = None, + use_site: bool = True, + only_values: bool = True, +) -> dict[str, Any]: + """Universal function to receive all addon settings. - Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id. - reference_types: Optional[Iterable[ActivityReferenceType]]: Filter - by reference types. - fields (Optional[Iterable[str]]): Fields that should be received - for each activity. + Based on 'project_name' will receive studio settings or project + settings. In case project is not passed is 'site_id' ignored. - Returns: - Optional[dict[str, Any]]: Activity data or None if activity is not - found. + Warnings: + Behavior of this function changed with AYON server version 0.3.0. + Structure of output from server changed. If using + 'only_values=True' then output should be same as before. + + Args: + bundle_name (Optional[str]): Name of bundle for which should be + settings received. + project_name (Optional[str]): Name of project for which should be + settings received. + variant (Optional[Literal['production', 'staging']]): Name of + settings variant. Used 'default_settings_variant' by default. + site_id (Optional[str]): Id of site for which want to receive + site overrides. + use_site (Optional[bool]): To force disable option of using site + overrides set to 'False'. In that case won't be applied + any site overrides. + only_values (Optional[bool]): Only settings values will be + returned. By default, is set to 'True'. """ con = get_server_api_connection() - return con.get_activity_by_id( + return con.get_addons_settings( + bundle_name=bundle_name, project_name=project_name, - activity_id=activity_id, - reference_types=reference_types, - fields=fields, + variant=variant, + site_id=site_id, + use_site=use_site, + only_values=only_values, ) -def create_activity( - 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, -) -> str: - """Create activity on a project. +def get_event( + event_id: str, +) -> Optional[dict[str, Any]]: + """Query full event data by id. + + Events received using event server do not contain full information. To + get the full event information is required to receive it explicitly. 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. + event_id (str): Event id. Returns: - str: Activity id. + dict[str, Any]: Full event data. """ con = get_server_api_connection() - return con.create_activity( - project_name=project_name, - entity_id=entity_id, - entity_type=entity_type, - activity_type=activity_type, - activity_id=activity_id, - body=body, - file_ids=file_ids, - timestamp=timestamp, - data=data, + return con.get_event( + event_id=event_id, ) -def update_activity( - project_name: str, - activity_id: str, - body: Optional[str] = None, - file_ids: Optional[list[str]] = None, - append_file_ids: Optional[bool] = False, - data: Optional[dict[str, Any]] = 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. - 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() - return con.update_activity( - project_name=project_name, - activity_id=activity_id, - body=body, - file_ids=file_ids, - append_file_ids=append_file_ids, - data=data, - ) - +def get_events( + topics: Optional[Iterable[str]] = None, + event_ids: Optional[Iterable[str]] = None, + project_names: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + users: Optional[Iterable[str]] = None, + include_logs: Optional[bool] = None, + has_children: Optional[bool] = None, + newer_than: Optional[str] = None, + older_than: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + limit: Optional[int] = None, + order: Optional[SortOrder] = None, + states: Optional[Iterable[str]] = None, +) -> Generator[dict[str, Any], None, None]: + """Get events from server with filtering options. -def delete_activity( - project_name: str, - activity_id: str, -): - """Delete activity by id. + Notes: + Not all event happen on a project. Args: - project_name (str): Project on which activity happened. - activity_id (str): Activity id to remove. + topics (Optional[Iterable[str]]): Name of topics. + event_ids (Optional[Iterable[str]]): Event ids. + project_names (Optional[Iterable[str]]): Project on which + event happened. + statuses (Optional[Iterable[str]]): Filtering by statuses. + users (Optional[Iterable[str]]): Filtering by users + who created/triggered an event. + include_logs (Optional[bool]): Query also log events. + has_children (Optional[bool]): Event is with/without children + events. If 'None' then all events are returned, default. + newer_than (Optional[str]): Return only events newer than given + iso datetime string. + older_than (Optional[str]): Return only events older than given + iso datetime string. + fields (Optional[Iterable[str]]): Fields that should be received + for each event. + limit (Optional[int]): Limit number of events to be fetched. + order (Optional[SortOrder]): Order events in ascending + or descending order. It is recommended to set 'limit' + when used descending. + states (Optional[Iterable[str]]): DEPRECATED Filtering by states. + Use 'statuses' instead. + + Returns: + Generator[dict[str, Any]]: Available events matching filters. """ con = get_server_api_connection() - return con.delete_activity( - project_name=project_name, - activity_id=activity_id, + return con.get_events( + topics=topics, + event_ids=event_ids, + project_names=project_names, + statuses=statuses, + users=users, + include_logs=include_logs, + has_children=has_children, + newer_than=newer_than, + older_than=older_than, + fields=fields, + limit=limit, + order=order, + states=states, ) -def send_activities_batch_operations( - project_name: str, - operations: list, - can_fail: bool = False, - raise_on_fail: bool = True, -) -> list: - """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. +def update_event( + event_id: str, + sender: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + status: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + progress: Optional[int] = None, + retries: Optional[int] = None, +): + """Update event data. 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. + event_id (str): Event id. + sender (Optional[str]): New sender of event. + project_name (Optional[str]): New project name. + username (Optional[str]): New username. + status (Optional[str]): New event status. Enum: "pending", + "in_progress", "finished", "failed", "aborted", "restarted" + description (Optional[str]): New description. + summary (Optional[dict[str, Any]]): New summary. + payload (Optional[dict[str, Any]]): New payload. + progress (Optional[int]): New progress. Range [0-100]. + retries (Optional[int]): New retries. """ con = get_server_api_connection() - return con.send_activities_batch_operations( + return con.update_event( + event_id=event_id, + sender=sender, project_name=project_name, - operations=operations, - can_fail=can_fail, - raise_on_fail=raise_on_fail, + username=username, + status=status, + description=description, + summary=summary, + payload=payload, + progress=progress, + retries=retries, ) -def get_addon_endpoint( - addon_name: str, - addon_version: str, - *subpaths, -) -> str: - """Calculate endpoint to addon route. - - Examples: - >>> from ayon_api import ServerAPI - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'addons/example/1.0.0/private/my.zip' +def dispatch_event( + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, +): + """Dispatch event to server. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (Optional[bool]): Mark event as finished on dispatch. + store (Optional[bool]): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. Returns: - str: Final url. - - """ - con = get_server_api_connection() - return con.get_addon_endpoint( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - ) - - -def get_addons_info( - details: bool = True, -) -> "AddonsInfoDict": - """Get information about addons available on server. - - Args: - details (Optional[bool]): Detailed data with information how - to get client code. + RestApiResponse: Response from server. """ con = get_server_api_connection() - return con.get_addons_info( - details=details, + return con.dispatch_event( + topic=topic, + sender=sender, + event_hash=event_hash, + project_name=project_name, + username=username, + depends_on=depends_on, + description=description, + summary=summary, + payload=payload, + finished=finished, + store=store, + dependencies=dependencies, ) -def get_addon_url( - addon_name: str, - addon_version: str, - *subpaths, - use_rest: bool = True, -) -> str: - """Calculate url to addon route. - - Examples: +def delete_event( + event_id: str, +): + """Delete event by id. - >>> api = ServerAPI("https://your.url.com") - >>> api.get_addon_url( - ... "example", "1.0.0", "private", "my.zip") - 'https://your.url.com/api/addons/example/1.0.0/private/my.zip' + Supported since AYON server 1.6.0. Args: - addon_name (str): Name of addon. - addon_version (str): Version of addon. - *subpaths (str): Any amount of subpaths that are added to - addon url. - use_rest (Optional[bool]): Use rest endpoint. + event_id (str): Event id. Returns: - str: Final url. + RestApiResponse: Response from server. """ con = get_server_api_connection() - return con.get_addon_url( - addon_name=addon_name, - addon_version=addon_version, - *subpaths, - use_rest=use_rest, + return con.delete_event( + event_id=event_id, ) -def delete_addon( - addon_name: str, - purge: Optional[bool] = None, -) -> None: - """Delete addon from server. +def enroll_event_job( + source_topic: "Union[str, list[str]]", + target_topic: str, + sender: str, + description: Optional[str] = None, + sequential: Optional[bool] = None, + events_filter: Optional["EventFilter"] = None, + max_retries: Optional[int] = None, + ignore_older_than: Optional[str] = None, + ignore_sender_types: Optional[str] = None, +): + """Enroll job based on events. - Delete all versions of addon from server. + Enroll will find first unprocessed event with 'source_topic' and will + create new event with 'target_topic' for it and return the new event + data. - Args: - addon_name (str): Addon name. - purge (Optional[bool]): Purge all data related to the addon. + Use 'sequential' to control that only single target event is created + at same time. Creation of new target events is blocked while there is + at least one unfinished event with target topic, when set to 'True'. + This helps when order of events matter and more than one process using + the same target is running at the same time. - """ - con = get_server_api_connection() - return con.delete_addon( - addon_name=addon_name, - purge=purge, - ) + Make sure the new event has updated status to '"finished"' status + when you're done with logic + Target topic should not clash with other processes/services. -def delete_addon_version( - addon_name: str, - addon_version: str, - purge: Optional[bool] = None, -) -> None: - """Delete addon version from server. + Created target event have 'dependsOn' key where is id of source topic. - Delete all versions of addon from server. + Use-case: + - Service 1 is creating events with topic 'my.leech' + - Service 2 process 'my.leech' and uses target topic 'my.process' + - this service can run on 1-n machines + - all events must be processed in a sequence by their creation + time and only one event can be processed at a time + - in this case 'sequential' should be set to 'True' so only + one machine is actually processing events, but if one goes + down there are other that can take place + - Service 3 process 'my.leech' and uses target topic 'my.discover' + - this service can run on 1-n machines + - order of events is not important + - 'sequential' should be 'False' Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - purge (Optional[bool]): Purge all data related to the addon. + source_topic (Union[str, list[str]]): Source topic to enroll with + wildcards '*', or explicit list of topics. + target_topic (str): Topic of dependent event. + sender (str): Identifier of sender (e.g. service name or username). + description (Optional[str]): Human readable text shown + in target event. + sequential (Optional[bool]): The source topic must be processed + in sequence. + events_filter (Optional[dict[str, Any]]): Filtering conditions + to filter the source event. For more technical specifications + look to server backed 'ayon_server.sqlfilter.Filter'. + TODO: Add example of filters. + max_retries (Optional[int]): How many times can be event retried. + Default value is based on server (3 at the time of this PR). + ignore_older_than (Optional[int]): Ignore events older than + given number in days. + ignore_sender_types (Optional[list[str]]): Ignore events triggered + by given sender types. + + Returns: + Optional[dict[str, Any]]: None if there is no event matching + filters. Created event with 'target_topic'. """ con = get_server_api_connection() - return con.delete_addon_version( - addon_name=addon_name, - addon_version=addon_version, - purge=purge, + return con.enroll_event_job( + source_topic=source_topic, + target_topic=target_topic, + sender=sender, + description=description, + sequential=sequential, + events_filter=events_filter, + max_retries=max_retries, + ignore_older_than=ignore_older_than, + ignore_sender_types=ignore_sender_types, ) -def upload_addon_zip( - src_filepath: str, - progress: Optional[TransferProgress] = None, +def get_attributes_schema( + use_cache: bool = True, +) -> "AttributesSchemaDict": + con = get_server_api_connection() + return con.get_attributes_schema( + use_cache=use_cache, + ) + + +def reset_attributes_schema(): + con = get_server_api_connection() + return con.reset_attributes_schema() + + +def set_attribute_config( + attribute_name: str, + data: "AttributeSchemaDataDict", + scope: list["AttributeScope"], + position: Optional[int] = None, + builtin: bool = False, ): - """Upload addon zip file to server. + con = get_server_api_connection() + return con.set_attribute_config( + attribute_name=attribute_name, + data=data, + scope=scope, + position=position, + builtin=builtin, + ) - File is validated on server. If it is valid, it is installed. It will - create an event job which can be tracked (tracking part is not - implemented yet). - Example output:: +def remove_attribute_config( + attribute_name: str, +): + """Remove attribute from server. - {'eventId': 'a1bfbdee27c611eea7580242ac120003'} + This can't be un-done, please use carefully. Args: - src_filepath (str): Path to a zip file. - progress (Optional[TransferProgress]): Object to keep track about - upload state. - - Returns: - dict[str, Any]: Response data from server. + attribute_name (str): Name of attribute to remove. """ con = get_server_api_connection() - return con.upload_addon_zip( - src_filepath=src_filepath, - progress=progress, + return con.remove_attribute_config( + attribute_name=attribute_name, ) -def download_addon_private_file( - addon_name: str, - addon_version: str, - filename: str, - destination_dir: str, - destination_filename: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, -) -> str: - """Download a file from addon private files. +def get_attributes_for_type( + entity_type: "AttributeScope", +) -> dict[str, "AttributeSchemaDict"]: + """Get attribute schemas available for an entity type. - This method requires to have authorized token available. Private files - are not under '/api' restpoint. + Example:: + + ``` + # Example attribute schema + { + # Common + "type": "integer", + "title": "Clip Out", + "description": null, + "example": 1, + "default": 1, + # These can be filled based on value of 'type' + "gt": null, + "ge": null, + "lt": null, + "le": null, + "minLength": null, + "maxLength": null, + "minItems": null, + "maxItems": null, + "regex": null, + "enum": null + } + ``` Args: - addon_name (str): Addon name. - addon_version (str): Addon version. - filename (str): Filename in private folder on server. - destination_dir (str): Where the file should be downloaded. - destination_filename (Optional[str]): Name of destination - filename. Source filename is used if not passed. - chunk_size (Optional[int]): Download chunk size. - progress (Optional[TransferProgress]): Object that gives ability - to track download progress. + entity_type (str): Entity type for which should be attributes + received. Returns: - str: Filepath to downloaded file. + dict[str, dict[str, Any]]: Attribute schemas that are available + for entered entity type. """ con = get_server_api_connection() - return con.download_addon_private_file( - addon_name=addon_name, - addon_version=addon_version, - filename=filename, - destination_dir=destination_dir, - destination_filename=destination_filename, - chunk_size=chunk_size, - progress=progress, + return con.get_attributes_for_type( + entity_type=entity_type, ) -def get_event( - event_id: str, -) -> Optional[dict[str, Any]]: - """Query full event data by id. +def get_attributes_fields_for_type( + entity_type: "AttributeScope", +) -> set[str]: + """Prepare attribute fields for entity type. - Events received using event server do not contain full information. To - get the full event information is required to receive it explicitly. + Returns: + set[str]: Attributes fields for entity type. - Args: - event_id (str): Event id. + """ + con = get_server_api_connection() + return con.get_attributes_fields_for_type( + entity_type=entity_type, + ) + + +def get_project_anatomy_presets() -> list["AnatomyPresetDict"]: + """Anatomy presets available on server. + + Content has basic information about presets. Example output:: + + [ + { + "name": "netflix_VFX", + "primary": false, + "version": "1.0.0" + }, + { + ... + }, + ... + ] Returns: - dict[str, Any]: Full event data. + list[dict[str, str]]: Anatomy presets available on server. """ con = get_server_api_connection() - return con.get_event( - event_id=event_id, - ) + return con.get_project_anatomy_presets() -def get_events( - topics: Optional[Iterable[str]] = None, - event_ids: Optional[Iterable[str]] = None, - project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - users: Optional[Iterable[str]] = None, - include_logs: Optional[bool] = None, - has_children: Optional[bool] = None, - newer_than: Optional[str] = None, - older_than: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - limit: Optional[int] = None, - order: Optional[SortOrder] = None, - states: Optional[Iterable[str]] = None, -) -> Generator[dict[str, Any], None, None]: - """Get events from server with filtering options. +def get_default_anatomy_preset_name() -> str: + """Name of default anatomy preset. - Notes: - Not all event happen on a project. + Primary preset is used as default preset. But when primary preset is + not set a built-in is used instead. Built-in preset is named '_'. - Args: - topics (Optional[Iterable[str]]): Name of topics. - event_ids (Optional[Iterable[str]]): Event ids. - project_names (Optional[Iterable[str]]): Project on which - event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. - users (Optional[Iterable[str]]): Filtering by users - who created/triggered an event. - include_logs (Optional[bool]): Query also log events. - has_children (Optional[bool]): Event is with/without children - events. If 'None' then all events are returned, default. - newer_than (Optional[str]): Return only events newer than given - iso datetime string. - older_than (Optional[str]): Return only events older than given - iso datetime string. - fields (Optional[Iterable[str]]): Fields that should be received - for each event. - limit (Optional[int]): Limit number of events to be fetched. - order (Optional[SortOrder]): Order events in ascending - or descending order. It is recommended to set 'limit' - when used descending. - states (Optional[Iterable[str]]): DEPRECATED Filtering by states. - Use 'statuses' instead. + Returns: + str: Name of preset that can be used by + 'get_project_anatomy_preset'. + + """ + con = get_server_api_connection() + return con.get_default_anatomy_preset_name() + + +def get_project_anatomy_preset( + preset_name: Optional[str] = None, +) -> "AnatomyPresetDict": + """Anatomy preset values by name. + + Get anatomy preset values by preset name. Primary preset is returned + if preset name is set to 'None'. + + Args: + preset_name (Optional[str]): Preset name. Returns: - Generator[dict[str, Any]]: Available events matching filters. + AnatomyPresetDict: Anatomy preset values. """ con = get_server_api_connection() - return con.get_events( - topics=topics, - event_ids=event_ids, - project_names=project_names, - statuses=statuses, - users=users, - include_logs=include_logs, - has_children=has_children, - newer_than=newer_than, - older_than=older_than, - fields=fields, - limit=limit, - order=order, - states=states, + return con.get_project_anatomy_preset( + preset_name=preset_name, ) -def update_event( - event_id: str, - sender: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - status: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[dict[str, Any]] = None, - payload: Optional[dict[str, Any]] = None, - progress: Optional[int] = None, - retries: Optional[int] = None, -): - """Update event data. +def get_built_in_anatomy_preset() -> "AnatomyPresetDict": + """Get built-in anatomy preset. + + Returns: + AnatomyPresetDict: Built-in anatomy preset. + + """ + con = get_server_api_connection() + return con.get_built_in_anatomy_preset() + + +def get_build_in_anatomy_preset() -> "AnatomyPresetDict": + con = get_server_api_connection() + return con.get_build_in_anatomy_preset() + + +def get_rest_project( + project_name: str, +) -> Optional["ProjectDict"]: + """Query project by name. + + This call returns project with anatomy data. Args: - event_id (str): Event id. - sender (Optional[str]): New sender of event. - project_name (Optional[str]): New project name. - username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", - "in_progress", "finished", "failed", "aborted", "restarted" - description (Optional[str]): New description. - summary (Optional[dict[str, Any]]): New summary. - payload (Optional[dict[str, Any]]): New payload. - progress (Optional[int]): New progress. Range [0-100]. - retries (Optional[int]): New retries. + project_name (str): Name of project. + + Returns: + Optional[ProjectDict]: Project entity data or 'None' if + project was not found. """ con = get_server_api_connection() - return con.update_event( - event_id=event_id, - sender=sender, + return con.get_rest_project( project_name=project_name, - username=username, - status=status, - description=description, - summary=summary, - payload=payload, - progress=progress, - retries=retries, ) -def dispatch_event( - topic: str, - sender: Optional[str] = None, - event_hash: Optional[str] = None, - project_name: Optional[str] = None, - username: Optional[str] = None, - depends_on: Optional[str] = None, - description: Optional[str] = None, - summary: Optional[dict[str, Any]] = None, - payload: Optional[dict[str, Any]] = None, - finished: bool = True, - store: bool = True, - dependencies: Optional[list[str]] = None, -): - """Dispatch event to server. +def get_rest_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> Generator["ProjectDict", None, None]: + """Query available project entities. + + User must be logged in. Args: - topic (str): Event topic used for filtering of listeners. - sender (Optional[str]): Sender of event. - event_hash (Optional[str]): Event hash. - project_name (Optional[str]): Project name. - depends_on (Optional[str]): Add dependency to another event. - username (Optional[str]): Username which triggered event. - description (Optional[str]): Description of event. - summary (Optional[dict[str, Any]]): Summary of event that can - be used for simple filtering on listeners. - payload (Optional[dict[str, Any]]): Full payload of event data with - all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible - future processing otherwise is event send only - to active listeners. - dependencies (Optional[list[str]]): Deprecated. - List of event id dependencies. + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. Returns: - RestApiResponse: Response from server. + Generator[ProjectDict, None, None]: Available projects. """ con = get_server_api_connection() - return con.dispatch_event( - topic=topic, - sender=sender, - event_hash=event_hash, - project_name=project_name, - username=username, - depends_on=depends_on, - description=description, - summary=summary, - payload=payload, - finished=finished, - store=store, - dependencies=dependencies, + return con.get_rest_projects( + active=active, + library=library, ) -def delete_event( - event_id: str, -): - """Delete event by id. +def get_project_names( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> list[str]: + """Receive available project names. - Supported since AYON server 1.6.0. + User must be logged in. Args: - event_id (str): Event id. + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. Returns: - RestApiResponse: Response from server. + list[str]: List of available project names. """ con = get_server_api_connection() - return con.delete_event( - event_id=event_id, + return con.get_project_names( + active=active, + library=library, ) -def enroll_event_job( - source_topic: "Union[str, list[str]]", - target_topic: str, - sender: str, - description: Optional[str] = None, - sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, - max_retries: Optional[int] = None, - ignore_older_than: Optional[str] = None, - ignore_sender_types: Optional[str] = None, -): - """Enroll job based on events. +def get_projects( + active: Optional[bool] = True, + library: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Generator["ProjectDict", None, None]: + """Get projects. - Enroll will find first unprocessed event with 'source_topic' and will - create new event with 'target_topic' for it and return the new event - data. + Args: + active (Optional[bool]): Filter active or inactive projects. + Filter is disabled when 'None' is passed. + library (Optional[bool]): Filter library projects. Filter is + disabled when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. - Use 'sequential' to control that only single target event is created - at same time. Creation of new target events is blocked while there is - at least one unfinished event with target topic, when set to 'True'. - This helps when order of events matter and more than one process using - the same target is running at the same time. + Returns: + Generator[ProjectDict, None, None]: Queried projects. - Make sure the new event has updated status to '"finished"' status - when you're done with logic + """ + con = get_server_api_connection() + return con.get_projects( + active=active, + library=library, + fields=fields, + own_attributes=own_attributes, + ) - Target topic should not clash with other processes/services. - Created target event have 'dependsOn' key where is id of source topic. +def get_project( + project_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["ProjectDict"]: + """Get project. - Use-case: - - Service 1 is creating events with topic 'my.leech' - - Service 2 process 'my.leech' and uses target topic 'my.process' - - this service can run on 1-n machines - - all events must be processed in a sequence by their creation - time and only one event can be processed at a time - - in this case 'sequential' should be set to 'True' so only - one machine is actually processing events, but if one goes - down there are other that can take place - - Service 3 process 'my.leech' and uses target topic 'my.discover' - - this service can run on 1-n machines - - order of events is not important - - 'sequential' should be 'False' + Args: + project_name (str): Name of project. + fields (Optional[Iterable[str]]): fields to be queried + for project. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[ProjectDict]: Project entity data or None + if project was not found. + + """ + con = get_server_api_connection() + return con.get_project( + project_name=project_name, + fields=fields, + own_attributes=own_attributes, + ) + + +def create_project( + project_name: str, + project_code: str, + library_project: bool = False, + preset_name: Optional[str] = None, +) -> "ProjectDict": + """Create project using AYON settings. + + This project creation function is not validating project entity on + creation. It is because project entity is created blindly with only + minimum required information about project which is name and code. + + Entered project name must be unique and project must not exist yet. + + Note: + This function is here to be OP v4 ready but in v3 has more logic + to do. That's why inner imports are in the body. Args: - source_topic (Union[str, list[str]]): Source topic to enroll with - wildcards '*', or explicit list of topics. - target_topic (str): Topic of dependent event. - sender (str): Identifier of sender (e.g. service name or username). - description (Optional[str]): Human readable text shown - in target event. - sequential (Optional[bool]): The source topic must be processed - in sequence. - events_filter (Optional[dict[str, Any]]): Filtering conditions - to filter the source event. For more technical specifications - look to server backed 'ayon_server.sqlfilter.Filter'. - TODO: Add example of filters. - max_retries (Optional[int]): How many times can be event retried. - Default value is based on server (3 at the time of this PR). - ignore_older_than (Optional[int]): Ignore events older than - given number in days. - ignore_sender_types (Optional[list[str]]): Ignore events triggered - by given sender types. + project_name (str): New project name. Should be unique. + project_code (str): Project's code should be unique too. + library_project (Optional[bool]): Project is library project. + preset_name (Optional[str]): Name of anatomy preset. Default is + used if not passed. + + Raises: + ValueError: When project name already exists. Returns: - Optional[dict[str, Any]]: None if there is no event matching - filters. Created event with 'target_topic'. + ProjectDict: Created project entity. """ con = get_server_api_connection() - return con.enroll_event_job( - source_topic=source_topic, - target_topic=target_topic, - sender=sender, - description=description, - sequential=sequential, - events_filter=events_filter, - max_retries=max_retries, - ignore_older_than=ignore_older_than, - ignore_sender_types=ignore_sender_types, + return con.create_project( + project_name=project_name, + project_code=project_code, + library_project=library_project, + preset_name=preset_name, + ) + + +def update_project( + project_name: str, + library: Optional[bool] = None, + folder_types: Optional[list[dict[str, Any]]] = None, + task_types: Optional[list[dict[str, Any]]] = None, + link_types: Optional[list[dict[str, Any]]] = None, + statuses: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[dict[str, Any]]] = None, + config: Optional[dict[str, Any]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + active: Optional[bool] = None, + project_code: Optional[str] = None, + **changes, +): + """Update project entity on server. + + Args: + project_name (str): Name of project. + library (Optional[bool]): Change library state. + folder_types (Optional[list[dict[str, Any]]]): Folder type + definitions. + task_types (Optional[list[dict[str, Any]]]): Task type + definitions. + link_types (Optional[list[dict[str, Any]]]): Link type + definitions. + statuses (Optional[list[dict[str, Any]]]): Status definitions. + tags (Optional[list[dict[str, Any]]]): List of tags available to + set on entities. + config (Optional[dict[str, Any]]): Project anatomy config + with templates and roots. + attrib (Optional[dict[str, Any]]): Project attributes to change. + data (Optional[dict[str, Any]]): Custom data of a project. This + value will 100% override project data. + active (Optional[bool]): Change active state of a project. + project_code (Optional[str]): Change project code. Not recommended + during production. + **changes: Other changed keys based on Rest API documentation. + + """ + con = get_server_api_connection() + return con.update_project( + project_name=project_name, + library=library, + folder_types=folder_types, + task_types=task_types, + link_types=link_types, + statuses=statuses, + tags=tags, + config=config, + attrib=attrib, + data=data, + active=active, + project_code=project_code, + **changes, ) -def get_full_link_type_name( - link_type_name: str, - input_type: str, - output_type: str, -) -> str: - """Calculate full link type name used for query from server. +def delete_project( + project_name: str, +): + """Delete project from server. - Args: - link_type_name (str): Type of link. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + This will completely remove project from server without any step back. - Returns: - str: Full name of link type used for query from server. + Args: + project_name (str): Project name that will be removed. """ con = get_server_api_connection() - return con.get_full_link_type_name( - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + return con.delete_project( + project_name=project_name, ) -def get_link_types( +def get_project_root_overrides( project_name: str, -) -> list[dict[str, Any]]: - """All link types available on a project. +) -> dict[str, dict[str, str]]: + """Root overrides per site name. - Example output: - [ - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } - ] + Method is based on logged user and can't be received for any other + user on server. + + Output will contain only roots per site id used by logged user. Args: - project_name (str): Name of project where to look for link types. + project_name (str): Name of project. Returns: - list[dict[str, Any]]: Link types available on project. + dict[str, dict[str, str]]: Root values by root name by site id. """ con = get_server_api_connection() - return con.get_link_types( + return con.get_project_root_overrides( project_name=project_name, ) -def get_link_type( +def get_project_roots_by_site( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -) -> Optional[dict[str, Any]]: - """Get link type data. +) -> dict[str, dict[str, str]]: + """Root overrides per site name. - There is not dedicated REST endpoint to get single link type, - so method 'get_link_types' is used. + Method is based on logged user and can't be received for any other + user on server. - Example output: - { - "name": "reference|folder|folder", - "link_type": "reference", - "input_type": "folder", - "output_type": "folder", - "data": {} - } + Output will contain only roots per site id used by logged user. + + Deprecated: + Use 'get_project_root_overrides' instead. Function + deprecated since 1.0.6 Args: - project_name (str): Project where link type is available. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + project_name (str): Name of project. Returns: - Optional[dict[str, Any]]: Link type information. + dict[str, dict[str, str]]: Root values by root name by site id. """ con = get_server_api_connection() - return con.get_link_type( + return con.get_project_roots_by_site( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, ) -def create_link_type( +def get_project_root_overrides_by_site_id( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[dict[str, Any]] = None, -): - """Create or update link type on server. + site_id: Optional[str] = None, +) -> dict[str, str]: + """Root overrides for site. - Warning: - Because PUT is used for creation it is also used for update. + If site id is not passed a site set in current api object is used + instead. Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Additional data related to link. + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. - Raises: - HTTPRequestError: Server error happened. + Returns: + dict[str, str]: Root values by root name or None if + site does not have overrides. """ con = get_server_api_connection() - return con.create_link_type( + return con.get_project_root_overrides_by_site_id( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + site_id=site_id, ) -def delete_link_type( +def get_project_roots_for_site( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, -): - """Remove link type from project. + site_id: Optional[str] = None, +) -> dict[str, str]: + """Root overrides for site. + + If site id is not passed a site set in current api object is used + instead. + Deprecated: + Use 'get_project_root_overrides_by_site_id' instead. Function + deprecated since 1.0.6 Args: - project_name (str): Project where link type is created. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. + project_name (str): Name of project. + site_id (Optional[str]): Site id for which want to receive + site overrides. - Raises: - HTTPRequestError: Server error happened. + Returns: + dict[str, str]: Root values by root name, root name is not + available if it does not have overrides. """ con = get_server_api_connection() - return con.delete_link_type( + return con.get_project_roots_for_site( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, + site_id=site_id, ) -def make_sure_link_type_exists( +def get_project_roots_by_site_id( project_name: str, - link_type_name: str, - input_type: str, - output_type: str, - data: Optional[dict[str, Any]] = None, -): - """Make sure link type exists on a project. + site_id: Optional[str] = None, +) -> dict[str, str]: + """Root values for a site. + + If site id is not passed a site set in current api object is used + instead. If site id is not available, default roots are returned + for current platform. Args: project_name (str): Name of project. - link_type_name (str): Name of link type. - input_type (str): Input entity type of link. - output_type (str): Output entity type of link. - data (Optional[dict[str, Any]]): Link type related data. + site_id (Optional[str]): Site id for which want to receive + root values. + + Returns: + dict[str, str]: Root values. """ con = get_server_api_connection() - return con.make_sure_link_type_exists( + return con.get_project_roots_by_site_id( project_name=project_name, - link_type_name=link_type_name, - input_type=input_type, - output_type=output_type, - data=data, + site_id=site_id, ) -def create_link( +def get_project_roots_by_platform( project_name: str, - link_type_name: str, - input_id: str, - input_type: str, - output_id: str, - output_type: str, - link_name: Optional[str] = None, -): - """Create link between 2 entities. - - Link has a type which must already exists on a project. + platform_name: Optional[str] = None, +) -> dict[str, str]: + """Root values for a site. - Example output:: + If platform name is not passed current platform name is used instead. - { - "id": "59a212c0d2e211eda0e20242ac120002" - } + This function does return root values without site overrides. It is + possible to use the function to receive default root values. Args: - project_name (str): Project where the link is created. - link_type_name (str): Type of link. - input_id (str): Input entity id. - input_type (str): Entity type of input entity. - output_id (str): Output entity id. - output_type (str): Entity type of output entity. - link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + project_name (str): Name of project. + platform_name (Optional[Literal["windows", "linux", "darwin"]]): + Platform name for which want to receive root values. Current + platform name is used if not passed. Returns: - dict[str, str]: Information about link. - - Raises: - HTTPRequestError: Server error happened. + dict[str, str]: Root values. """ con = get_server_api_connection() - return con.create_link( + return con.get_project_roots_by_platform( project_name=project_name, - link_type_name=link_type_name, - input_id=input_id, - input_type=input_type, - output_id=output_id, - output_type=output_type, - link_name=link_name, + platform_name=platform_name, + ) + + +def get_rest_folder( + project_name: str, + folder_id: str, +) -> Optional["FolderDict"]: + con = get_server_api_connection() + return con.get_rest_folder( + project_name=project_name, + folder_id=folder_id, ) -def delete_link( - project_name: str, - link_id: str, -): - """Remove link by id. +def get_rest_folders( + project_name: str, + include_attrib: bool = False, +) -> list["FlatFolderDict"]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] Args: - project_name (str): Project where link exists. - link_id (str): Id of link. + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. - Raises: - HTTPRequestError: Server error happened. + Returns: + list[FlatFolderDict]: List of folder entities. """ con = get_server_api_connection() - return con.delete_link( + return con.get_rest_folders( project_name=project_name, - link_id=link_id, + include_attrib=include_attrib, ) -def get_entities_links( +def get_folders_hierarchy( project_name: str, - entity_type: str, - entity_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, - link_names: Optional[Iterable[str]] = None, - link_name_regex: Optional[str] = None, -) -> dict[str, list[dict[str, Any]]]: - """Helper method to get links from server for entity types. + search_string: Optional[str] = None, + folder_types: Optional[Iterable[str]] = None, +) -> "ProjectHierarchyDict": + """Get project hierarchy. - .. highlight:: text - .. code-block:: text + All folders in project in hierarchy data structure. - Example output: + Example output: { - "59a212c0d2e211eda0e20242ac120001": [ + "hierarchy": [ { - "id": "59a212c0d2e211eda0e20242ac120002", - "linkType": "reference", - "description": "reference link between folders", - "projectName": "my_project", - "author": "frantadmin", - "entityId": "b1df109676db11ed8e8c6c9466b19aa8", - "entityType": "folder", - "direction": "out" + "id": "...", + "name": "...", + "label": "...", + "status": "...", + "folderType": "...", + "hasTasks": False, + "taskNames": [], + "parents": [], + "parentId": None, + "children": [...children folders...] }, ... - ], - ... + ] } Args: - project_name (str): Project where links are. - entity_type (Literal["folder", "task", "product", - "version", "representations"]): Entity type. - entity_ids (Optional[Iterable[str]]): Ids of entities for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - link_names (Optional[Iterable[str]]): Link name filters. - link_name_regex (Optional[str]): Regex filter for link name. + project_name (str): Project where to look for folders. + search_string (Optional[str]): Search string to filter folders. + folder_types (Optional[Iterable[str]]): Folder types to filter. Returns: - dict[str, list[dict[str, Any]]]: Link info by entity ids. + dict[str, Any]: Response data from server. """ con = get_server_api_connection() - return con.get_entities_links( + return con.get_folders_hierarchy( project_name=project_name, - entity_type=entity_type, - entity_ids=entity_ids, - link_types=link_types, - link_direction=link_direction, - link_names=link_names, - link_name_regex=link_name_regex, + search_string=search_string, + folder_types=folder_types, ) -def get_folders_links( +def get_folders_rest( project_name: str, - folder_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query folders links from server. + include_attrib: bool = False, +) -> list["FlatFolderDict"]: + """Get simplified flat list of all project folders. + + Get all project folders in single REST call. This can be faster than + using 'get_folders' method which is using GraphQl, but does not + allow any filtering, and set of fields is defined + by server backend. + + Example:: + + [ + { + "id": "112233445566", + "parentId": "112233445567", + "path": "/root/parent/child", + "parents": ["root", "parent"], + "name": "child", + "label": "Child", + "folderType": "Folder", + "hasTasks": False, + "hasChildren": False, + "taskNames": [ + "Compositing", + ], + "status": "In Progress", + "attrib": {}, + "ownAttrib": [], + "updatedAt": "2023-06-12T15:37:02.420260", + }, + ... + ] + + Deprecated: + Use 'get_rest_folders' instead. Function was renamed to match + other rest functions, like 'get_rest_folder', + 'get_rest_project' etc. . + Will be removed in '1.0.7' or '1.1.0'. Args: - project_name (str): Project where links are. - folder_ids (Optional[Iterable[str]]): Ids of folders for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name. + include_attrib (Optional[bool]): Include attribute values + in output. Slower to query. Returns: - dict[str, list[dict[str, Any]]]: Link info by folder ids. + list[FlatFolderDict]: List of folder entities. """ con = get_server_api_connection() - return con.get_folders_links( + return con.get_folders_rest( project_name=project_name, - folder_ids=folder_ids, - link_types=link_types, - link_direction=link_direction, + include_attrib=include_attrib, ) -def get_folder_links( +def get_folders( project_name: str, - folder_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query folder links from server. + folder_ids: Optional[Iterable[str]] = None, + folder_paths: Optional[Iterable[str]] = None, + folder_names: Optional[Iterable[str]] = None, + folder_types: Optional[Iterable[str]] = None, + parent_ids: Optional[Iterable[str]] = None, + folder_path_regex: Optional[str] = None, + has_products: Optional[bool] = None, + has_tasks: Optional[bool] = None, + has_children: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + has_links: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Generator["FolderDict", None, None]: + """Query folders from server. + + Todos: + Folder name won't be unique identifier, so we should add + folder path filtering. + + Notes: + Filter 'active' don't have direct filter in GraphQl. Args: - project_name (str): Project where links are. - folder_id (str): Folder id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Folder ids to filter. + folder_paths (Optional[Iterable[str]]): Folder paths used + for filtering. + folder_names (Optional[Iterable[str]]): Folder names used + for filtering. + folder_types (Optional[Iterable[str]]): Folder types used + for filtering. + parent_ids (Optional[Iterable[str]]): Ids of folder parents. + Use 'None' if folder is direct child of project. + folder_path_regex (Optional[str]): Folder path regex used + for filtering. + has_products (Optional[bool]): Filter folders with/without + products. Ignored when None, default behavior. + has_tasks (Optional[bool]): Filter folders with/without + tasks. Ignored when None, default behavior. + has_children (Optional[bool]): Filter folders with/without + children. Ignored when None, default behavior. + statuses (Optional[Iterable[str]]): Folder statuses used + for filtering. + assignees_all (Optional[Iterable[str]]): Filter by assigness + on children tasks. Task must have all of passed assignees. + tags (Optional[Iterable[str]]): Folder tags used + for filtering. + active (Optional[bool]): Filter active/inactive folders. + Both are returned if is set to None. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - list[dict[str, Any]]: Link info of folder. + Generator[FolderDict, None, None]: Queried folder entities. """ con = get_server_api_connection() - return con.get_folder_links( + return con.get_folders( project_name=project_name, - folder_id=folder_id, - link_types=link_types, - link_direction=link_direction, + folder_ids=folder_ids, + folder_paths=folder_paths, + folder_names=folder_names, + folder_types=folder_types, + parent_ids=parent_ids, + folder_path_regex=folder_path_regex, + has_products=has_products, + has_tasks=has_tasks, + has_children=has_children, + statuses=statuses, + assignees_all=assignees_all, + tags=tags, + active=active, + has_links=has_links, + fields=fields, + own_attributes=own_attributes, ) -def get_tasks_links( +def get_folder_by_id( project_name: str, - task_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query tasks links from server. + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["FolderDict"]: + """Query folder entity by id. Args: - project_name (str): Project where links are. - task_ids (Optional[Iterable[str]]): Ids of tasks for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - dict[str, list[dict[str, Any]]]: Link info by task ids. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_tasks_links( + return con.get_folder_by_id( project_name=project_name, - task_ids=task_ids, - link_types=link_types, - link_direction=link_direction, + folder_id=folder_id, + fields=fields, + own_attributes=own_attributes, ) -def get_task_links( +def get_folder_by_path( project_name: str, - task_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query task links from server. + folder_path: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["FolderDict"]: + """Query folder entity by path. + + Folder path is a path to folder with all parent names joined by slash. Args: - project_name (str): Project where links are. - task_id (str): Task id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where to look for queried + entities. + folder_path (str): Folder path. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - list[dict[str, Any]]: Link info of task. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_task_links( + return con.get_folder_by_path( project_name=project_name, - task_id=task_id, - link_types=link_types, - link_direction=link_direction, + folder_path=folder_path, + fields=fields, + own_attributes=own_attributes, ) -def get_products_links( +def get_folder_by_name( project_name: str, - product_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query products links from server. + folder_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["FolderDict"]: + """Query folder entity by path. + + Warnings: + Folder name is not a unique identifier of a folder. Function is + kept for OpenPype 3 compatibility. Args: - project_name (str): Project where links are. - product_ids (Optional[Iterable[str]]): Ids of products for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project where to look for queried + entities. + folder_name (str): Folder name. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - dict[str, list[dict[str, Any]]]: Link info by product ids. + Optional[FolderDict]: Folder entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_products_links( + return con.get_folder_by_name( project_name=project_name, - product_ids=product_ids, - link_types=link_types, - link_direction=link_direction, + folder_name=folder_name, + fields=fields, + own_attributes=own_attributes, ) -def get_product_links( +def get_folder_ids_with_products( project_name: str, - product_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query product links from server. + folder_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """Find folders which have at least one product. + + Folders that have at least one product should be immutable, so they + should not change path -> change of name or name of any parent + is not possible. Args: - project_name (str): Project where links are. - product_id (str): Product id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Name of project. + folder_ids (Optional[Iterable[str]]): Limit folder ids filtering + to a set of folders. If set to None all folders on project are + checked. Returns: - list[dict[str, Any]]: Link info of product. + set[str]: Folder ids that have at least one product. """ con = get_server_api_connection() - return con.get_product_links( + return con.get_folder_ids_with_products( project_name=project_name, - product_id=product_id, - link_types=link_types, - link_direction=link_direction, + folder_ids=folder_ids, ) -def get_versions_links( +def create_folder( project_name: str, - version_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query versions links from server. + name: str, + folder_type: Optional[str] = None, + parent_id: Optional[str] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + folder_id: Optional[str] = None, +) -> str: + """Create new folder. Args: - project_name (str): Project where links are. - version_ids (Optional[Iterable[str]]): Ids of versions for which - links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + project_name (str): Project name. + name (str): Folder name. + folder_type (Optional[str]): Folder type. + parent_id (Optional[str]): Parent folder id. Parent is project + if is ``None``. + label (Optional[str]): Label of folder. + attrib (Optional[dict[str, Any]]): Folder attributes. + data (Optional[dict[str, Any]]): Folder data. + tags (Optional[Iterable[str]]): Folder tags. + status (Optional[str]): Folder status. + active (Optional[bool]): Folder active state. + thumbnail_id (Optional[str]): Folder thumbnail id. + folder_id (Optional[str]): Folder id. If not passed new id is + generated. Returns: - dict[str, list[dict[str, Any]]]: Link info by version ids. + str: Entity id. """ con = get_server_api_connection() - return con.get_versions_links( + return con.create_folder( project_name=project_name, - version_ids=version_ids, - link_types=link_types, - link_direction=link_direction, + name=name, + folder_type=folder_type, + parent_id=parent_id, + label=label, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + folder_id=folder_id, ) -def get_version_links( +def update_folder( project_name: str, - version_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query version links from server. + folder_id: str, + name: Optional[str] = None, + folder_type: Optional[str] = None, + parent_id: Optional[str] = NOT_SET, + label: Optional[str] = NOT_SET, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, +): + """Update folder entity on server. - Args: - project_name (str): Project where links are. - version_id (str): Version id for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. + Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. - Returns: - list[dict[str, Any]]: Link info of version. + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + name (Optional[str]): New name. + folder_type (Optional[str]): New folder type. + parent_id (Optional[str]): New parent folder id. + label (Optional[str]): New label. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.get_version_links( + return con.update_folder( project_name=project_name, - version_id=version_id, - link_types=link_types, - link_direction=link_direction, + folder_id=folder_id, + name=name, + folder_type=folder_type, + parent_id=parent_id, + label=label, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, ) -def get_representations_links( +def delete_folder( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> dict[str, list[dict[str, Any]]]: - """Query representations links from server. + folder_id: str, + force: bool = False, +): + """Delete folder. Args: - project_name (str): Project where links are. - representation_ids (Optional[Iterable[str]]): Ids of - representations for which links should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - dict[str, list[dict[str, Any]]]: Link info by representation ids. + project_name (str): Project name. + folder_id (str): Folder id to delete. + force (Optional[bool]): Folder delete folder with all children + folder, products, versions and representations. """ con = get_server_api_connection() - return con.get_representations_links( + return con.delete_folder( project_name=project_name, - representation_ids=representation_ids, - link_types=link_types, - link_direction=link_direction, + folder_id=folder_id, + force=force, ) -def get_representation_links( +def get_rest_task( project_name: str, - representation_id: str, - link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, -) -> list[dict[str, Any]]: - """Query representation links from server. - - Args: - project_name (str): Project where links are. - representation_id (str): Representation id for which links - should be received. - link_types (Optional[Iterable[str]]): Link type filters. - link_direction (Optional[Literal["in", "out"]]): Link direction - filter. - - Returns: - list[dict[str, Any]]: Link info of representation. - - """ + task_id: str, +) -> Optional["TaskDict"]: con = get_server_api_connection() - return con.get_representation_links( + return con.get_rest_task( project_name=project_name, - representation_id=representation_id, - link_types=link_types, - link_direction=link_direction, + task_id=task_id, ) -def get_entity_lists( +def get_tasks( project_name: str, - *, - list_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = None, + task_ids: Optional[Iterable[str]] = None, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, -) -> Generator[Dict[str, Any], None, None]: - """Fetch entity lists from server. + own_attributes: bool = False, +) -> Generator["TaskDict", None, None]: + """Query task entities from server. Args: - project_name (str): Project name where entity lists are. - list_ids (Optional[Iterable[str]]): List of entity list ids to - fetch. - active (Optional[bool]): Filter by active state of entity lists. - fields (Optional[Iterable[str]]): Fields to fetch from server. + project_name (str): Name of project. + task_ids (Iterable[str]): Task ids to filter. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + folder_ids (Iterable[str]): Ids of task parents. Use 'None' + if folder is direct child of project. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Generator[Dict[str, Any], None, None]: Entity list entities - matching defined filters. + Generator[TaskDict, None, None]: Queried task entities. """ con = get_server_api_connection() - return con.get_entity_lists( + return con.get_tasks( project_name=project_name, - list_ids=list_ids, + task_ids=task_ids, + task_names=task_names, + task_types=task_types, + folder_ids=folder_ids, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, active=active, fields=fields, + own_attributes=own_attributes, ) -def get_entity_list_rest( +def get_task_by_name( project_name: str, - list_id: str, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using REST API. + folder_id: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by name and folder id. Args: - project_name (str): Project name. - list_id (str): Entity list id. + project_name (str): Name of project where to look for queried + entities. + folder_id (str): Folder id. + task_name (str): Task name + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_entity_list_rest( + return con.get_task_by_name( project_name=project_name, - list_id=list_id, + folder_id=folder_id, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, ) -def get_entity_list_by_id( +def get_task_by_id( project_name: str, - list_id: str, + task_id: str, fields: Optional[Iterable[str]] = None, -) -> Optional[Dict[str, Any]]: - """Get entity list by id using GraphQl. + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by id. Args: - project_name (str): Project name. - list_id (str): Entity list id. - fields (Optional[Iterable[str]]): Fields to fetch from server. + project_name (str): Name of project where to look for queried + entities. + task_id (str): Task id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. Returns: - Optional[Dict[str, Any]]: Entity list data or None if not found. + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.get_entity_list_by_id( + return con.get_task_by_id( project_name=project_name, - list_id=list_id, + task_id=task_id, fields=fields, + own_attributes=own_attributes, ) -def create_entity_list( +def get_tasks_by_folder_paths( project_name: str, - entity_type: "EntityListEntityType", - label: str, - *, - list_type: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - template: Optional[Dict[str, Any]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, - items: Optional[List[Dict[str, Any]]] = None, - list_id: Optional[str] = None, -) -> str: - """Create entity list. + folder_paths: Iterable[str], + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> dict[str, list["TaskDict"]]: + """Query task entities from server by folder paths. Args: - project_name (str): Project name where entity list lives. - entity_type (EntityListEntityType): Which entity types can be - used in list. - label (str): Entity list label. - list_type (Optional[str]): Entity list type. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - template (Optional[dict[str, Any]]): Dynamic list template. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. - items (Optional[list[dict[str, Any]]]): Initial items in - entity list. - list_id (Optional[str]): Entity list id. + project_name (str): Name of project. + folder_paths (list[str]): Folder paths. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + dict[str, list[TaskDict]]: Task entities by + folder path. """ con = get_server_api_connection() - return con.create_entity_list( + return con.get_tasks_by_folder_paths( project_name=project_name, - entity_type=entity_type, - label=label, - list_type=list_type, - access=access, - attrib=attrib, - data=data, + folder_paths=folder_paths, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, tags=tags, - template=template, - owner=owner, active=active, - items=items, - list_id=list_id, + fields=fields, + own_attributes=own_attributes, ) -def update_entity_list( +def get_tasks_by_folder_path( project_name: str, - list_id: str, - *, - label: Optional[str] = None, - access: Optional[Dict[str, Any]] = None, - attrib: Optional[List[Dict[str, Any]]] = None, - data: Optional[List[Dict[str, Any]]] = None, - tags: Optional[List[str]] = None, - owner: Optional[str] = None, - active: Optional[bool] = None, -) -> None: - """Update entity list. + folder_path: str, + task_names: Optional[Iterable[str]] = None, + task_types: Optional[Iterable[str]] = None, + assignees: Optional[Iterable[str]] = None, + assignees_all: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> list["TaskDict"]: + """Query task entities from server by folder path. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id that will be updated. - label (Optional[str]): New label of entity list. - access (Optional[dict[str, Any]]): Access control for entity list. - attrib (Optional[dict[str, Any]]): Attribute values of - entity list. - data (Optional[dict[str, Any]]): Custom data of entity list. - tags (Optional[list[str]]): Entity list tags. - owner (Optional[str]): New owner of the list. - active (Optional[bool]): Change active state of entity list. + project_name (str): Name of project. + folder_path (str): Folder path. + task_names (Iterable[str]): Task names used for filtering. + task_types (Iterable[str]): Task types used for filtering. + assignees (Optional[Iterable[str]]): Task assignees used for + filtering. All tasks with any of passed assignees are + returned. + assignees_all (Optional[Iterable[str]]): Task assignees used + for filtering. Task must have all of passed assignees to be + returned. + statuses (Optional[Iterable[str]]): Task statuses used for + filtering. + tags (Optional[Iterable[str]]): Task tags used for + filtering. + active (Optional[bool]): Filter active/inactive tasks. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. """ con = get_server_api_connection() - return con.update_entity_list( + return con.get_tasks_by_folder_path( project_name=project_name, - list_id=list_id, - label=label, - access=access, - attrib=attrib, - data=data, + folder_path=folder_path, + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, tags=tags, - owner=owner, active=active, + fields=fields, + own_attributes=own_attributes, ) -def delete_entity_list( +def get_task_by_folder_path( project_name: str, - list_id: str, -) -> None: - """Delete entity list from project. + folder_path: str, + task_name: str, + fields: Optional[Iterable[str]] = None, + own_attributes: bool = False, +) -> Optional["TaskDict"]: + """Query task entity by folder path and task name. Args: project_name (str): Project name. - list_id (str): Entity list id that will be removed. + folder_path (str): Folder path. + task_name (str): Task name. + fields (Optional[Iterable[str]]): Task fields that should + be returned. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Optional[TaskDict]: Task entity data or None if was not found. """ con = get_server_api_connection() - return con.delete_entity_list( + return con.get_task_by_folder_path( project_name=project_name, - list_id=list_id, + folder_path=folder_path, + task_name=task_name, + fields=fields, + own_attributes=own_attributes, ) -def get_entity_list_attribute_definitions( +def create_task( project_name: str, - list_id: str, -) -> List["EntityListAttributeDefinitionDict"]: - """Get attribute definitioins on entity list. + name: str, + task_type: str, + folder_id: str, + label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, +) -> str: + """Create new task. Args: project_name (str): Project name. - list_id (str): Entity list id. + name (str): Folder name. + task_type (str): Task type. + folder_id (str): Parent folder id. + label (Optional[str]): Label of folder. + assignees (Optional[Iterable[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[Iterable[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is + generated. Returns: - List[EntityListAttributeDefinitionDict]: List of attribute - definitions. + str: Task id. """ con = get_server_api_connection() - return con.get_entity_list_attribute_definitions( + return con.create_task( project_name=project_name, - list_id=list_id, + name=name, + task_type=task_type, + folder_id=folder_id, + label=label, + assignees=assignees, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + task_id=task_id, ) -def set_entity_list_attribute_definitions( +def update_task( project_name: str, - list_id: str, - attribute_definitions: List["EntityListAttributeDefinitionDict"], -) -> None: - """Set attribute definitioins on entity list. - - Args: - project_name (str): Project name. - list_id (str): Entity list id. - attribute_definitions (List[EntityListAttributeDefinitionDict]): - List of attribute definitions. + task_id: str, + name: Optional[str] = None, + task_type: Optional[str] = None, + folder_id: Optional[str] = None, + label: Optional[str] = NOT_SET, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, +): + """Update task entity on server. - """ - con = get_server_api_connection() - return con.set_entity_list_attribute_definitions( - project_name=project_name, - list_id=list_id, - attribute_definitions=attribute_definitions, - ) + Do not pass ``label`` amd ``thumbnail_id`` if you don't + want to change their values. Value ``None`` would unset + their value. + Update of ``data`` will override existing value on folder entity. -def create_entity_list_item( - project_name: str, - list_id: str, - *, - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, - item_id: Optional[str] = None, -) -> str: - """Create entity list item. + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. Args: - project_name (str): Project name where entity list lives. - list_id (str): Entity list id where item will be added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Item attribute values. - data (Optional[dict[str, Any]]): Item data. - tags (Optional[list[str]]): Tags of item in entity list. - item_id (Optional[str]): Id of item that will be created. - - Returns: - str: Item id. + project_name (str): Project name. + task_id (str): Task id. + name (Optional[str]): New name. + task_type (Optional[str]): New task type. + folder_id (Optional[str]): New folder id. + label (Optional[Optional[str]]): New label. + assignees (Optional[str]): New assignees. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.create_entity_list_item( + return con.update_task( project_name=project_name, - list_id=list_id, - position=position, + task_id=task_id, + name=name, + task_type=task_type, + folder_id=folder_id, label=label, + assignees=assignees, attrib=attrib, data=data, tags=tags, - item_id=item_id, + status=status, + active=active, + thumbnail_id=thumbnail_id, ) -def update_entity_list_items( +def delete_task( project_name: str, - list_id: str, - items: List[Dict[str, Any]], - mode: "EntityListItemMode", -) -> None: - """Update items in entity list. + task_id: str, +): + """Delete task. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id. - items (List[Dict[str, Any]]): Entity list items. - mode (EntityListItemMode): Mode of items update. + project_name (str): Project name. + task_id (str): Task id to delete. """ con = get_server_api_connection() - return con.update_entity_list_items( + return con.delete_task( project_name=project_name, - list_id=list_id, - items=items, - mode=mode, + task_id=task_id, ) -def update_entity_list_item( +def get_rest_product( project_name: str, - list_id: str, - item_id: str, - *, - new_list_id: Optional[str], - position: Optional[int] = None, - label: Optional[str] = None, - attrib: Optional[Dict[str, Any]] = None, - data: Optional[Dict[str, Any]] = None, - tags: Optional[List[str]] = None, -) -> None: - """Update item in entity list. - - Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id where item lives. - item_id (str): Item id that will be removed from entity list. - new_list_id (Optional[str]): New entity list id where item will be - added. - position (Optional[int]): Position of item in entity list. - label (Optional[str]): Label of item in entity list. - attrib (Optional[dict[str, Any]]): Attributes of item in entity - list. - data (Optional[dict[str, Any]]): Custom data of item in - entity list. - tags (Optional[list[str]]): Tags of item in entity list. - - """ + product_id: str, +) -> Optional["ProductDict"]: con = get_server_api_connection() - return con.update_entity_list_item( + return con.get_rest_product( project_name=project_name, - list_id=list_id, - item_id=item_id, - new_list_id=new_list_id, - position=position, - label=label, - attrib=attrib, - data=data, - tags=tags, + product_id=product_id, ) -def delete_entity_list_item( +def get_products( project_name: str, - list_id: str, - item_id: str, -) -> None: - """Delete item from entity list. + product_ids: Optional[Iterable[str]] = None, + product_names: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + product_types: Optional[Iterable[str]] = None, + product_name_regex: Optional[str] = None, + product_path_regex: Optional[str] = None, + names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["ProductDict", None, None]: + """Query products from server. + + Todos: + Separate 'name_by_folder_ids' filtering to separated method. It + cannot be combined with some other filters. Args: - project_name (str): Project name where entity list live. - list_id (str): Entity list id from which item will be removed. - item_id (str): Item id that will be removed from entity list. + project_name (str): Name of project. + product_ids (Optional[Iterable[str]]): Task ids to filter. + product_names (Optional[Iterable[str]]): Task names used for + filtering. + folder_ids (Optional[Iterable[str]]): Ids of task parents. + Use 'None' if folder is direct child of project. + product_types (Optional[Iterable[str]]): Product types used for + filtering. + product_name_regex (Optional[str]): Filter products by name regex. + product_path_regex (Optional[str]): Filter products by path regex. + Path starts with folder path and ends with product name. + names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product + name filtering by folder id. + statuses (Optional[Iterable[str]]): Product statuses used + for filtering. + tags (Optional[Iterable[str]]): Product tags used + for filtering. + active (Optional[bool]): Filter active/inactive products. + Both are returned if is set to None. + fields (Optional[Iterable[str]]): Fields to be queried for + folder. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. + + Returns: + Generator[ProductDict, None, None]: Queried product entities. """ con = get_server_api_connection() - return con.delete_entity_list_item( + return con.get_products( project_name=project_name, - list_id=list_id, - item_id=item_id, + product_ids=product_ids, + product_names=product_names, + folder_ids=folder_ids, + product_types=product_types, + product_name_regex=product_name_regex, + product_path_regex=product_path_regex, + names_by_folder_ids=names_by_folder_ids, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_rest_project( +def get_product_by_id( project_name: str, -) -> Optional["ProjectDict"]: - """Query project by name. - - This call returns project with anatomy data. + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["ProductDict"]: + """Query product entity by id. Args: - project_name (str): Name of project. + project_name (str): Name of project where to look for queried + entities. + product_id (str): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - Optional[ProjectDict]: Project entity data or 'None' if - project was not found. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_rest_project( + return con.get_product_by_id( project_name=project_name, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def get_rest_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, -) -> Generator["ProjectDict", None, None]: - """Query available project entities. - - User must be logged in. +def get_product_by_name( + project_name: str, + product_name: str, + folder_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["ProductDict"]: + """Query product entity by name and folder id. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. + project_name (str): Name of project where to look for queried + entities. + product_name (str): Product name. + folder_id (str): Folder id (Folder is a parent of products). + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + products. Returns: - Generator[ProjectDict, None, None]: Available projects. + Optional[ProductDict]: Product entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_rest_projects( - active=active, - library=library, + return con.get_product_by_name( + project_name=project_name, + product_name=product_name, + folder_id=folder_id, + fields=fields, + own_attributes=own_attributes, ) -def get_project_names( - active: Optional[bool] = True, - library: Optional[bool] = None, -) -> list[str]: - """Receive available project names. +def get_product_types( + fields: Optional[Iterable[str]] = None, +) -> list["ProductTypeDict"]: + """Types of products. - User must be logged in. + This is server wide information. Product types have 'name', 'icon' and + 'color'. Args: - active (Optional[bool]): Filter active/inactive projects. Both - are returned if 'None' is passed. - library (Optional[bool]): Filter standard/library projects. Both - are returned if 'None' is passed. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - list[str]: List of available project names. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_project_names( - active=active, - library=library, + return con.get_product_types( + fields=fields, ) -def get_projects( - active: Optional[bool] = True, - library: Optional[bool] = None, +def get_project_product_types( + project_name: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["ProjectDict", None, None]: - """Get projects. +) -> list["ProductTypeDict"]: + """DEPRECATED Types of products available in a project. + + Filter only product types available in a project. Args: - active (Optional[bool]): Filter active or inactive projects. - Filter is disabled when 'None' is passed. - library (Optional[bool]): Filter library projects. Filter is - disabled when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Name of the project where to look for + product types. + fields (Optional[Iterable[str]]): Product types fields to query. Returns: - Generator[ProjectDict, None, None]: Queried projects. + list[ProductTypeDict]: Product types information. """ con = get_server_api_connection() - return con.get_projects( - active=active, - library=library, + return con.get_project_product_types( + project_name=project_name, fields=fields, - own_attributes=own_attributes, ) -def get_project( - project_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["ProjectDict"]: - """Get project. +def get_product_type_names( + project_name: Optional[str] = None, + product_ids: Optional[Iterable[str]] = None, +) -> set[str]: + """DEPRECATED Product type names. + + Warnings: + This function will be probably removed. Matters if 'products_id' + filter has real use-case. Args: - project_name (str): Name of project. - fields (Optional[Iterable[str]]): fields to be queried - for project. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (Optional[str]): Name of project where to look for + queried entities. + product_ids (Optional[Iterable[str]]): Product ids filter. Can be + used only with 'project_name'. Returns: - Optional[ProjectDict]: Project entity data or None - if project was not found. + set[str]: Product type names. """ con = get_server_api_connection() - return con.get_project( + return con.get_product_type_names( project_name=project_name, - fields=fields, - own_attributes=own_attributes, + product_ids=product_ids, ) -def create_project( +def create_product( project_name: str, - project_code: str, - library_project: bool = False, - preset_name: Optional[str] = None, -) -> "ProjectDict": - """Create project using AYON settings. - - This project creation function is not validating project entity on - creation. It is because project entity is created blindly with only - minimum required information about project which is name and code. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. + name: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + product_id: Optional[str] = None, +) -> str: + """Create new product. Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (Optional[bool]): Project is library project. - preset_name (Optional[str]): Name of anatomy preset. Default is - used if not passed. - - Raises: - ValueError: When project name already exists. + project_name (str): Project name. + name (str): Product name. + product_type (str): Product type. + folder_id (str): Parent folder id. + attrib (Optional[dict[str, Any]]): Product attributes. + data (Optional[dict[str, Any]]): Product data. + tags (Optional[Iterable[str]]): Product tags. + status (Optional[str]): Product status. + active (Optional[bool]): Product active state. + product_id (Optional[str]): Product id. If not passed new id is + generated. Returns: - ProjectDict: Created project entity. + str: Product id. """ con = get_server_api_connection() - return con.create_project( + return con.create_product( project_name=project_name, - project_code=project_code, - library_project=library_project, - preset_name=preset_name, + name=name, + product_type=product_type, + folder_id=folder_id, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + product_id=product_id, ) -def update_project( +def update_product( project_name: str, - library: Optional[bool] = None, - folder_types: Optional[list[dict[str, Any]]] = None, - task_types: Optional[list[dict[str, Any]]] = None, - link_types: Optional[list[dict[str, Any]]] = None, - statuses: Optional[list[dict[str, Any]]] = None, - tags: Optional[list[dict[str, Any]]] = None, - config: Optional[dict[str, Any]] = None, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, active: Optional[bool] = None, - project_code: Optional[str] = None, - **changes, ): - """Update project entity on server. + """Update product entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. Args: - project_name (str): Name of project. - library (Optional[bool]): Change library state. - folder_types (Optional[list[dict[str, Any]]]): Folder type - definitions. - task_types (Optional[list[dict[str, Any]]]): Task type - definitions. - link_types (Optional[list[dict[str, Any]]]): Link type - definitions. - statuses (Optional[list[dict[str, Any]]]): Status definitions. - tags (Optional[list[dict[str, Any]]]): List of tags available to - set on entities. - config (Optional[dict[str, Any]]): Project anatomy config - with templates and roots. - attrib (Optional[dict[str, Any]]): Project attributes to change. - data (Optional[dict[str, Any]]): Custom data of a project. This - value will 100% override project data. - active (Optional[bool]): Change active state of a project. - project_code (Optional[str]): Change project code. Not recommended - during production. - **changes: Other changed keys based on Rest API documentation. + project_name (str): Project name. + product_id (str): Product id. + name (Optional[str]): New product name. + folder_id (Optional[str]): New product id. + product_type (Optional[str]): New product type. + attrib (Optional[dict[str, Any]]): New product attributes. + data (Optional[dict[str, Any]]): New product data. + tags (Optional[Iterable[str]]): New product tags. + status (Optional[str]): New product status. + active (Optional[bool]): New product active state. """ con = get_server_api_connection() - return con.update_project( + return con.update_product( project_name=project_name, - library=library, - folder_types=folder_types, - task_types=task_types, - link_types=link_types, - statuses=statuses, - tags=tags, - config=config, + product_id=product_id, + name=name, + folder_id=folder_id, + product_type=product_type, attrib=attrib, data=data, + tags=tags, + status=status, active=active, - project_code=project_code, - **changes, ) -def delete_project( +def delete_product( project_name: str, + product_id: str, ): - """Delete project from server. + """Delete product. - This will completely remove project from server without any step back. + Args: + project_name (str): Project name. + product_id (str): Product id to delete. + + """ + con = get_server_api_connection() + return con.delete_product( + project_name=project_name, + product_id=product_id, + ) + + +def get_rest_version( + project_name: str, + version_id: str, +) -> Optional["VersionDict"]: + con = get_server_api_connection() + return con.get_rest_version( + project_name=project_name, + version_id=version_id, + ) + + +def get_versions( + project_name: str, + version_ids: Optional[Iterable[str]] = None, + product_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + versions: Optional[Iterable[str]] = None, + hero: bool = True, + standard: bool = True, + latest: Optional[bool] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + active: Optional[bool] = True, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator["VersionDict", None, None]: + """Get version entities based on passed filters from server. Args: - project_name (str): Project name that will be removed. + project_name (str): Name of project where to look for versions. + version_ids (Optional[Iterable[str]]): Version ids used for + version filtering. + product_ids (Optional[Iterable[str]]): Product ids used for + version filtering. + task_ids (Optional[Iterable[str]]): Task ids used for + version filtering. + versions (Optional[Iterable[int]]): Versions we're interested in. + hero (Optional[bool]): Skip hero versions when set to False. + standard (Optional[bool]): Skip standard (non-hero) when + set to False. + latest (Optional[bool]): Return only latest version of standard + versions. This can be combined only with 'standard' attribute + set to True. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields to be queried + for version. All possible folder fields are returned + if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Generator[VersionDict, None, None]: Queried version entities. """ con = get_server_api_connection() - return con.delete_project( + return con.get_versions( project_name=project_name, + version_ids=version_ids, + product_ids=product_ids, + task_ids=task_ids, + versions=versions, + hero=hero, + standard=standard, + latest=latest, + statuses=statuses, + tags=tags, + active=active, + fields=fields, + own_attributes=own_attributes, ) -def get_rest_folder( - project_name: str, - folder_id: str, -) -> Optional["FolderDict"]: +def get_version_by_id( + project_name: str, + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query version entity by id. + + Args: + project_name (str): Name of project where to look for queried + entities. + version_id (str): Version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. + + Returns: + Optional[VersionDict]: Version entity data or None + if was not found. + + """ con = get_server_api_connection() - return con.get_rest_folder( + return con.get_version_by_id( project_name=project_name, - folder_id=folder_id, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def get_rest_folders( +def get_version_by_name( project_name: str, - include_attrib: bool = False, -) -> list["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] + version: int, + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query version entity by version and product id. Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. + project_name (str): Name of project where to look for queried + entities. + version (int): Version of version entity. + product_id (str): Product id. Product is a parent of version. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - List[FlatFolderDict]: List of folder entities. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_rest_folders( + return con.get_version_by_name( project_name=project_name, - include_attrib=include_attrib, + version=version, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def get_folders_hierarchy( +def get_hero_version_by_id( project_name: str, - search_string: Optional[str] = None, - folder_types: Optional[Iterable[str]] = None, -) -> "ProjectHierarchyDict": - """Get project hierarchy. - - All folders in project in hierarchy data structure. - - Example output: - { - "hierarchy": [ - { - "id": "...", - "name": "...", - "label": "...", - "status": "...", - "folderType": "...", - "hasTasks": False, - "taskNames": [], - "parents": [], - "parentId": None, - "children": [...children folders...] - }, - ... - ] - } + version_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query hero version entity by id. Args: - project_name (str): Project where to look for folders. - search_string (Optional[str]): Search string to filter folders. - folder_types (Optional[Iterable[str]]): Folder types to filter. + project_name (str): Name of project where to look for queried + entities. + version_id (int): Hero version id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - dict[str, Any]: Response data from server. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_folders_hierarchy( + return con.get_hero_version_by_id( project_name=project_name, - search_string=search_string, - folder_types=folder_types, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, ) -def get_folders_rest( +def get_hero_version_by_product_id( project_name: str, - include_attrib: bool = False, -) -> list["FlatFolderDict"]: - """Get simplified flat list of all project folders. - - Get all project folders in single REST call. This can be faster than - using 'get_folders' method which is using GraphQl, but does not - allow any filtering, and set of fields is defined - by server backend. - - Example:: - - [ - { - "id": "112233445566", - "parentId": "112233445567", - "path": "/root/parent/child", - "parents": ["root", "parent"], - "name": "child", - "label": "Child", - "folderType": "Folder", - "hasTasks": False, - "hasChildren": False, - "taskNames": [ - "Compositing", - ], - "status": "In Progress", - "attrib": {}, - "ownAttrib": [], - "updatedAt": "2023-06-12T15:37:02.420260", - }, - ... - ] + product_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query hero version entity by product id. - Deprecated: - Use 'get_rest_folders' instead. Function was renamed to match - other rest functions, like 'get_rest_folder', - 'get_rest_project' etc. . - Will be removed in '1.0.7' or '1.1.0'. + Only one hero version is available on a product. Args: - project_name (str): Project name. - include_attrib (Optional[bool]): Include attribute values - in output. Slower to query. + project_name (str): Name of project where to look for queried + entities. + product_id (int): Product id. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - List[FlatFolderDict]: List of folder entities. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_folders_rest( + return con.get_hero_version_by_product_id( project_name=project_name, - include_attrib=include_attrib, + product_id=product_id, + fields=fields, + own_attributes=own_attributes, ) -def get_folders( +def get_hero_versions( project_name: str, - folder_ids: Optional[Iterable[str]] = None, - folder_paths: Optional[Iterable[str]] = None, - folder_names: Optional[Iterable[str]] = None, - folder_types: Optional[Iterable[str]] = None, - parent_ids: Optional[Iterable[str]] = None, - folder_path_regex: Optional[str] = None, - has_products: Optional[bool] = None, - has_tasks: Optional[bool] = None, - has_children: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, + product_ids: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, active: Optional[bool] = True, - has_links: Optional[bool] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["FolderDict", None, None]: - """Query folders from server. - - Todos: - Folder name won't be unique identifier, so we should add - folder path filtering. - - Notes: - Filter 'active' don't have direct filter in GraphQl. - - Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Folder ids to filter. - folder_paths (Optional[Iterable[str]]): Folder paths used - for filtering. - folder_names (Optional[Iterable[str]]): Folder names used - for filtering. - folder_types (Optional[Iterable[str]]): Folder types used - for filtering. - parent_ids (Optional[Iterable[str]]): Ids of folder parents. - Use 'None' if folder is direct child of project. - folder_path_regex (Optional[str]): Folder path regex used - for filtering. - has_products (Optional[bool]): Filter folders with/without - products. Ignored when None, default behavior. - has_tasks (Optional[bool]): Filter folders with/without - tasks. Ignored when None, default behavior. - has_children (Optional[bool]): Filter folders with/without - children. Ignored when None, default behavior. - statuses (Optional[Iterable[str]]): Folder statuses used - for filtering. - assignees_all (Optional[Iterable[str]]): Filter by assigness - on children tasks. Task must have all of passed assignees. - tags (Optional[Iterable[str]]): Folder tags used - for filtering. - active (Optional[bool]): Filter active/inactive folders. - Both are returned if is set to None. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + own_attributes=_PLACEHOLDER, +) -> Generator["VersionDict", None, None]: + """Query hero versions by multiple filters. + + Only one hero version is available on a product. + + Args: + project_name (str): Name of project where to look for queried + entities. + product_ids (Optional[Iterable[str]]): Product ids. + version_ids (Optional[Iterable[str]]): Version ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): Fields that should be returned. + All fields are returned if 'None' is passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - Generator[FolderDict, None, None]: Queried folder entities. + Optional[VersionDict]: Version entity data or None + if was not found. """ con = get_server_api_connection() - return con.get_folders( + return con.get_hero_versions( project_name=project_name, - folder_ids=folder_ids, - folder_paths=folder_paths, - folder_names=folder_names, - folder_types=folder_types, - parent_ids=parent_ids, - folder_path_regex=folder_path_regex, - has_products=has_products, - has_tasks=has_tasks, - has_children=has_children, - statuses=statuses, - assignees_all=assignees_all, - tags=tags, + product_ids=product_ids, + version_ids=version_ids, active=active, - has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_folder_by_id( +def get_last_versions( project_name: str, - folder_id: str, + product_ids: Iterable[str], + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by id. + own_attributes=_PLACEHOLDER, +) -> dict[str, Optional["VersionDict"]]: + """Query last version entities by product ids. Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for representation. + product_ids (Iterable[str]): Product ids. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + dict[str, Optional[VersionDict]]: Last versions by product id. """ con = get_server_api_connection() - return con.get_folder_by_id( + return con.get_last_versions( project_name=project_name, - folder_id=folder_id, + product_ids=product_ids, + active=active, fields=fields, own_attributes=own_attributes, ) -def get_folder_by_path( +def get_last_version_by_product_id( project_name: str, - folder_path: str, + product_id: str, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by path. - - Folder path is a path to folder with all parent names joined by slash. + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query last version entity by product id. Args: - project_name (str): Name of project where to look for queried - entities. - folder_path (str): Folder path. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for representation. + product_id (str): Product id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + versions. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_folder_by_path( + return con.get_last_version_by_product_id( project_name=project_name, - folder_path=folder_path, + product_id=product_id, + active=active, fields=fields, own_attributes=own_attributes, ) -def get_folder_by_name( +def get_last_version_by_product_name( project_name: str, - folder_name: str, + product_name: str, + folder_id: str, + active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["FolderDict"]: - """Query folder entity by path. - - Warnings: - Folder name is not a unique identifier of a folder. Function is - kept for OpenPype 3 compatibility. + own_attributes=_PLACEHOLDER, +) -> Optional["VersionDict"]: + """Query last version entity by product name and folder id. Args: - project_name (str): Name of project where to look for queried - entities. - folder_name (str): Folder name. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - Optional[FolderDict]: Folder entity data or None - if was not found. + Optional[VersionDict]: Queried version entity or None. """ con = get_server_api_connection() - return con.get_folder_by_name( + return con.get_last_version_by_product_name( project_name=project_name, - folder_name=folder_name, + product_name=product_name, + folder_id=folder_id, + active=active, fields=fields, own_attributes=own_attributes, ) -def get_folder_ids_with_products( +def version_is_latest( project_name: str, - folder_ids: Optional[Iterable[str]] = None, -) -> set[str]: - """Find folders which have at least one product. - - Folders that have at least one product should be immutable, so they - should not change path -> change of name or name of any parent - is not possible. + version_id: str, +) -> bool: + """Is version latest from a product. Args: - project_name (str): Name of project. - folder_ids (Optional[Iterable[str]]): Limit folder ids filtering - to a set of folders. If set to None all folders on project are - checked. + project_name (str): Project where to look for representation. + version_id (str): Version id. Returns: - set[str]: Folder ids that have at least one product. + bool: Version is latest or not. """ con = get_server_api_connection() - return con.get_folder_ids_with_products( + return con.version_is_latest( project_name=project_name, - folder_ids=folder_ids, + version_id=version_id, ) -def create_folder( +def create_version( project_name: str, - name: str, - folder_type: Optional[str] = None, - parent_id: Optional[str] = None, - label: Optional[str] = None, + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = None, - folder_id: Optional[str] = None, + version_id: Optional[str] = None, ) -> str: - """Create new folder. + """Create new version. Args: project_name (str): Project name. - name (str): Folder name. - folder_type (Optional[str]): Folder type. - parent_id (Optional[str]): Parent folder id. Parent is project - if is ``None``. - label (Optional[str]): Label of folder. - attrib (Optional[dict[str, Any]]): Folder attributes. - data (Optional[dict[str, Any]]): Folder data. - tags (Optional[Iterable[str]]): Folder tags. - status (Optional[str]): Folder status. - active (Optional[bool]): Folder active state. - thumbnail_id (Optional[str]): Folder thumbnail id. - folder_id (Optional[str]): Folder id. If not passed new id is + version (int): Version. + product_id (str): Parent product id. + task_id (Optional[str]): Parent task id. + author (Optional[str]): Version author. + attrib (Optional[dict[str, Any]]): Version attributes. + data (Optional[dict[str, Any]]): Version data. + tags (Optional[Iterable[str]]): Version tags. + status (Optional[str]): Version status. + active (Optional[bool]): Version active state. + thumbnail_id (Optional[str]): Version thumbnail id. + version_id (Optional[str]): Version id. If not passed new id is generated. Returns: - str: Entity id. + str: Version id. """ con = get_server_api_connection() - return con.create_folder( + return con.create_version( project_name=project_name, - name=name, - folder_type=folder_type, - parent_id=parent_id, - label=label, + version=version, + product_id=product_id, + task_id=task_id, + author=author, attrib=attrib, data=data, tags=tags, status=status, active=active, thumbnail_id=thumbnail_id, - folder_id=folder_id, + version_id=version_id, ) -def update_folder( +def update_version( project_name: str, - folder_id: str, - name: Optional[str] = None, - folder_type: Optional[str] = None, - parent_id: Optional[str] = NOT_SET, - label: Optional[str] = NOT_SET, + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + author: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, @@ -5192,9 +5479,9 @@ def update_folder( active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, ): - """Update folder entity on server. + """Update version entity on server. - Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't + Do not pass ``task_id`` amd ``thumbnail_id`` if you don't want to change their values. Value ``None`` would unset their value. @@ -5205,11 +5492,11 @@ def update_folder( Args: project_name (str): Project name. - folder_id (str): Folder id. - name (Optional[str]): New name. - folder_type (Optional[str]): New folder type. - parent_id (Optional[str]): New parent folder id. - label (Optional[str]): New label. + version_id (str): Version id. + version (Optional[int]): New version. + product_id (Optional[str]): New product id. + task_id (Optional[str]): New task id. + author (Optional[str]): New author username. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. @@ -5219,13 +5506,13 @@ def update_folder( """ con = get_server_api_connection() - return con.update_folder( + return con.update_version( project_name=project_name, - folder_id=folder_id, - name=name, - folder_type=folder_type, - parent_id=parent_id, - label=label, + version_id=version_id, + version=version, + product_id=product_id, + task_id=task_id, + author=author, attrib=attrib, data=data, tags=tags, @@ -5235,385 +5522,445 @@ def update_folder( ) -def delete_folder( +def delete_version( project_name: str, - folder_id: str, - force: bool = False, + version_id: str, ): - """Delete folder. + """Delete version. Args: project_name (str): Project name. - folder_id (str): Folder id to delete. - force (Optional[bool]): Folder delete folder with all children - folder, products, versions and representations. + version_id (str): Version id to delete. """ con = get_server_api_connection() - return con.delete_folder( + return con.delete_version( project_name=project_name, - folder_id=folder_id, - force=force, + version_id=version_id, ) -def get_rest_task( +def get_rest_representation( project_name: str, - task_id: str, -) -> Optional["TaskDict"]: + representation_id: str, +) -> Optional["RepresentationDict"]: con = get_server_api_connection() - return con.get_rest_task( + return con.get_rest_representation( project_name=project_name, - task_id=task_id, + representation_id=representation_id, ) -def get_tasks( +def get_representations( project_name: str, - task_ids: Optional[Iterable[str]] = None, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, + representation_ids: Optional[Iterable[str]] = None, + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, + names_by_version_ids: Optional[dict[str, Iterable[str]]] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Generator["TaskDict", None, None]: - """Query task entities from server. + own_attributes=_PLACEHOLDER, +) -> Generator["RepresentationDict", None, None]: + """Get representation entities based on passed filters from server. + + .. todo:: + + Add separated function for 'names_by_version_ids' filtering. + Because can't be combined with others. Args: - project_name (str): Name of project. - task_ids (Iterable[str]): Task ids to filter. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - folder_ids (Iterable[str]): Ids of task parents. Use 'None' - if folder is direct child of project. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. + project_name (str): Name of project where to look for versions. + representation_ids (Optional[Iterable[str]]): Representation ids + used for representation filtering. + representation_names (Optional[Iterable[str]]): Representation + names used for representation filtering. + version_ids (Optional[Iterable[str]]): Version ids used for + representation filtering. Versions are parents of + representations. + names_by_version_ids (Optional[dict[str, Iterable[str]]]): Find + representations by names and version ids. This filter + discards all other filters. + statuses (Optional[Iterable[str]]): Representation statuses used + for filtering. + tags (Optional[Iterable[str]]): Representation tags used + for filtering. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - Generator[TaskDict, None, None]: Queried task entities. + Generator[RepresentationDict, None, None]: Queried + representation entities. """ con = get_server_api_connection() - return con.get_tasks( + return con.get_representations( project_name=project_name, - task_ids=task_ids, - task_names=task_names, - task_types=task_types, - folder_ids=folder_ids, - assignees=assignees, - assignees_all=assignees_all, + representation_ids=representation_ids, + representation_names=representation_names, + version_ids=version_ids, + names_by_version_ids=names_by_version_ids, statuses=statuses, tags=tags, active=active, + has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_task_by_name( +def get_representation_by_id( project_name: str, - folder_id: str, - task_name: str, + representation_id: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by name and folder id. + own_attributes=_PLACEHOLDER, +) -> Optional["RepresentationDict"]: + """Query representation entity from server based on id filter. Args: - project_name (str): Name of project where to look for queried - entities. - folder_id (str): Folder id. - task_name (str): Task name - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for representation. + representation_id (str): Id of representation. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + Optional[RepresentationDict]: Queried representation + entity or None. """ con = get_server_api_connection() - return con.get_task_by_name( + return con.get_representation_by_id( project_name=project_name, - folder_id=folder_id, - task_name=task_name, + representation_id=representation_id, fields=fields, own_attributes=own_attributes, ) -def get_task_by_id( +def get_representation_by_name( project_name: str, - task_id: str, + representation_name: str, + version_id: str, fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by id. + own_attributes=_PLACEHOLDER, +) -> Optional["RepresentationDict"]: + """Query representation entity by name and version id. Args: - project_name (str): Name of project where to look for queried - entities. - task_id (str): Task id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for representation. + representation_name (str): Representation name. + version_id (str): Version id. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + representations. + + Returns: + Optional[RepresentationDict]: Queried representation entity + or None. + + """ + con = get_server_api_connection() + return con.get_representation_by_name( + project_name=project_name, + representation_name=representation_name, + version_id=version_id, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_representations_hierarchy( + project_name: str, + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> dict[str, RepresentationHierarchy]: + """Find representation with parents by representation id. + + Representation entity with parent entities up to project. + + Default fields are used when any fields are set to `None`. But it is + possible to pass in empty iterable (list, set, tuple) to skip + entity. + + Args: + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. + + Returns: + dict[str, RepresentationHierarchy]: Parent entities by + representation id. + + """ + con = get_server_api_connection() + return con.get_representations_hierarchy( + project_name=project_name, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, + ) + + +def get_representation_hierarchy( + project_name: str, + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + task_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, + representation_fields: Optional[Iterable[str]] = None, +) -> Optional[RepresentationHierarchy]: + """Find representation parents by representation id. + + Representation parent entities up to project. + + Args: + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + task_fields (Optional[Iterable[str]]): Task fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + representation_fields (Optional[Iterable[str]]): Representation + fields. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + RepresentationHierarchy: Representation hierarchy entities. """ con = get_server_api_connection() - return con.get_task_by_id( + return con.get_representation_hierarchy( project_name=project_name, - task_id=task_id, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + task_fields=task_fields, + product_fields=product_fields, + version_fields=version_fields, + representation_fields=representation_fields, ) -def get_tasks_by_folder_paths( +def get_representations_parents( project_name: str, - folder_paths: Iterable[str], - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> dict[str, list["TaskDict"]]: - """Query task entities from server by folder paths. + representation_ids: Iterable[str], + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> dict[str, RepresentationParents]: + """Find representations parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Name of project. - folder_paths (list[str]): Folder paths. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for entities. + representation_ids (Iterable[str]): Representation ids. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. Returns: - dict[str, list[TaskDict]]: Task entities by - folder path. + dict[str, RepresentationParents]: Parent entities by + representation id. """ con = get_server_api_connection() - return con.get_tasks_by_folder_paths( + return con.get_representations_parents( project_name=project_name, - folder_paths=folder_paths, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + representation_ids=representation_ids, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def get_tasks_by_folder_path( +def get_representation_parents( project_name: str, - folder_path: str, - task_names: Optional[Iterable[str]] = None, - task_types: Optional[Iterable[str]] = None, - assignees: Optional[Iterable[str]] = None, - assignees_all: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> list["TaskDict"]: - """Query task entities from server by folder path. + representation_id: str, + project_fields: Optional[Iterable[str]] = None, + folder_fields: Optional[Iterable[str]] = None, + product_fields: Optional[Iterable[str]] = None, + version_fields: Optional[Iterable[str]] = None, +) -> Optional["RepresentationParents"]: + """Find representation parents by representation id. + + Representation parent entities up to project. Args: - project_name (str): Name of project. - folder_path (str): Folder path. - task_names (Iterable[str]): Task names used for filtering. - task_types (Iterable[str]): Task types used for filtering. - assignees (Optional[Iterable[str]]): Task assignees used for - filtering. All tasks with any of passed assignees are - returned. - assignees_all (Optional[Iterable[str]]): Task assignees used - for filtering. Task must have all of passed assignees to be - returned. - statuses (Optional[Iterable[str]]): Task statuses used for - filtering. - tags (Optional[Iterable[str]]): Task tags used for - filtering. - active (Optional[bool]): Filter active/inactive tasks. - Both are returned if is set to None. - fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for entities. + representation_id (str): Representation id. + project_fields (Optional[Iterable[str]]): Project fields. + folder_fields (Optional[Iterable[str]]): Folder fields. + product_fields (Optional[Iterable[str]]): Product fields. + version_fields (Optional[Iterable[str]]): Version fields. + + Returns: + RepresentationParents: Representation parent entities. """ con = get_server_api_connection() - return con.get_tasks_by_folder_path( + return con.get_representation_parents( project_name=project_name, - folder_path=folder_path, - task_names=task_names, - task_types=task_types, - assignees=assignees, - assignees_all=assignees_all, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + representation_id=representation_id, + project_fields=project_fields, + folder_fields=folder_fields, + product_fields=product_fields, + version_fields=version_fields, ) -def get_task_by_folder_path( +def get_repre_ids_by_context_filters( project_name: str, - folder_path: str, - task_name: str, - fields: Optional[Iterable[str]] = None, - own_attributes: bool = False, -) -> Optional["TaskDict"]: - """Query task entity by folder path and task name. + context_filters: Optional[dict[str, Iterable[str]]], + representation_names: Optional[Iterable[str]] = None, + version_ids: Optional[Iterable[str]] = None, +) -> list[str]: + """Find representation ids which match passed context filters. + + Each representation has context integrated on representation entity in + database. The context may contain project, folder, task name or + product name, product type and many more. This implementation gives + option to quickly filter representation based on representation data + in database. + + Context filters have defined structure. To define filter of nested + subfield use dot '.' as delimiter (For example 'task.name'). + Filter values can be regex filters. String or ``re.Pattern`` can + be used. Args: - project_name (str): Project name. - folder_path (str): Folder path. - task_name (str): Task name. - fields (Optional[Iterable[str]]): Task fields that should - be returned. - own_attributes (Optional[bool]): Attribute values that are - not explicitly set on entity will have 'None' value. + project_name (str): Project where to look for representations. + context_filters (dict[str, list[str]]): Filters of context fields. + representation_names (Optional[Iterable[str]]): Representation + names, can be used as additional filter for representations + by their names. + version_ids (Optional[Iterable[str]]): Version ids, can be used + as additional filter for representations by their parent ids. Returns: - Optional[TaskDict]: Task entity data or None if was not found. + list[str]: Representation ids that match passed filters. + + Example: + The function returns just representation ids so if entities are + required for funtionality they must be queried afterwards by + their ids. + >>> from ayon_api import get_repre_ids_by_context_filters + >>> from ayon_api import get_representations + >>> project_name = "testProject" + >>> filters = { + ... "task.name": ["[aA]nimation"], + ... "product": [".*[Mm]ain"] + ... } + >>> repre_ids = get_repre_ids_by_context_filters( + ... project_name, filters) + >>> repres = get_representations(project_name, repre_ids) """ con = get_server_api_connection() - return con.get_task_by_folder_path( + return con.get_repre_ids_by_context_filters( project_name=project_name, - folder_path=folder_path, - task_name=task_name, - fields=fields, - own_attributes=own_attributes, + context_filters=context_filters, + representation_names=representation_names, + version_ids=version_ids, ) -def create_task( +def create_representation( project_name: str, name: str, - task_type: str, - folder_id: str, - label: Optional[str] = None, - assignees: Optional[Iterable[str]] = None, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - task_id: Optional[str] = None, + representation_id: Optional[str] = None, ) -> str: - """Create new task. + """Create new representation. Args: project_name (str): Project name. - name (str): Folder name. - task_type (str): Task type. - folder_id (str): Parent folder id. - label (Optional[str]): Label of folder. - assignees (Optional[Iterable[str]]): Task assignees. - attrib (Optional[dict[str, Any]]): Task attributes. - data (Optional[dict[str, Any]]): Task data. - tags (Optional[Iterable[str]]): Task tags. - status (Optional[str]): Task status. - active (Optional[bool]): Task active state. - thumbnail_id (Optional[str]): Task thumbnail id. - task_id (Optional[str]): Task id. If not passed new id is - generated. + name (str): Representation name. + version_id (str): Parent version id. + files (Optional[list[dict]]): Representation files information. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + traits (Optional[dict[str, Any]]): Representation traits + serialized data as dict. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + representation_id (Optional[str]): Representation id. If not + passed new id is generated. Returns: - str: Task id. + str: Representation id. """ con = get_server_api_connection() - return con.create_task( + return con.create_representation( project_name=project_name, name=name, - task_type=task_type, - folder_id=folder_id, - label=label, - assignees=assignees, + version_id=version_id, + files=files, attrib=attrib, data=data, + traits=traits, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, - task_id=task_id, + representation_id=representation_id, ) -def update_task( +def update_representation( project_name: str, - task_id: str, + representation_id: str, name: Optional[str] = None, - task_type: Optional[str] = None, - folder_id: Optional[str] = None, - label: Optional[str] = NOT_SET, - assignees: Optional[list[str]] = None, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, ): - """Update task entity on server. - - Do not pass ``label`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. + """Update representation entity on server. Update of ``data`` will override existing value on folder entity. @@ -5622,1700 +5969,1352 @@ def update_task( Args: project_name (str): Project name. - task_id (str): Task id. + representation_id (str): Representation id. name (Optional[str]): New name. - task_type (Optional[str]): New task type. - folder_id (Optional[str]): New folder id. - label (Optional[Optional[str]]): New label. - assignees (Optional[str]): New assignees. + version_id (Optional[str]): New version id. + files (Optional[list[dict]]): New files + information. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. + traits (Optional[dict[str, Any]]): New traits. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. - thumbnail_id (Optional[str]): New thumbnail id. """ con = get_server_api_connection() - return con.update_task( + return con.update_representation( project_name=project_name, - task_id=task_id, + representation_id=representation_id, name=name, - task_type=task_type, - folder_id=folder_id, - label=label, - assignees=assignees, + version_id=version_id, + files=files, attrib=attrib, data=data, + traits=traits, tags=tags, status=status, active=active, - thumbnail_id=thumbnail_id, ) -def delete_task( +def delete_representation( project_name: str, - task_id: str, + representation_id: str, ): - """Delete task. + """Delete representation. Args: project_name (str): Project name. - task_id (str): Task id to delete. + representation_id (str): Representation id to delete. """ con = get_server_api_connection() - return con.delete_task( - project_name=project_name, - task_id=task_id, - ) - - -def get_rest_product( - project_name: str, - product_id: str, -) -> Optional["ProductDict"]: - con = get_server_api_connection() - return con.get_rest_product( + return con.delete_representation( project_name=project_name, - product_id=product_id, + representation_id=representation_id, ) -def get_products( +def get_workfiles_info( project_name: str, - product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]] = None, - folder_ids: Optional[Iterable[str]] = None, - product_types: Optional[Iterable[str]] = None, - product_name_regex: Optional[str] = None, - product_path_regex: Optional[str] = None, - names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + paths: Optional[Iterable[str]] = None, + path_regex: Optional[str] = None, statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, - active: Optional[bool] = True, + has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["ProductDict", None, None]: - """Query products from server. - - Todos: - Separate 'name_by_folder_ids' filtering to separated method. It - cannot be combined with some other filters. +) -> Generator["WorkfileInfoDict", None, None]: + """Workfile info entities by passed filters. Args: - project_name (str): Name of project. - product_ids (Optional[Iterable[str]]): Task ids to filter. - product_names (Optional[Iterable[str]]): Task names used for - filtering. - folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of project. - product_types (Optional[Iterable[str]]): Product types used for - filtering. - product_name_regex (Optional[str]): Filter products by name regex. - product_path_regex (Optional[str]): Filter products by path regex. - Path starts with folder path and ends with product name. - names_by_folder_ids (Optional[dict[str, Iterable[str]]]): Product - name filtering by folder id. - statuses (Optional[Iterable[str]]): Product statuses used + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used for filtering. - tags (Optional[Iterable[str]]): Product tags used + tags (Optional[Iterable[str]]): Workfile info tags used for filtering. - active (Optional[bool]): Filter active/inactive products. - Both are returned if is set to None. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. fields (Optional[Iterable[str]]): Fields to be queried for - folder. All possible folder fields are returned - if 'None' is passed. + representation. All possible fields are returned if 'None' is + passed. own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. + workfiles. Returns: - Generator[ProductDict, None, None]: Queried product entities. + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. """ con = get_server_api_connection() - return con.get_products( + return con.get_workfiles_info( project_name=project_name, - product_ids=product_ids, - product_names=product_names, - folder_ids=folder_ids, - product_types=product_types, - product_name_regex=product_name_regex, - product_path_regex=product_path_regex, - names_by_folder_ids=names_by_folder_ids, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, statuses=statuses, tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_product_by_id( - project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by id. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_id (str): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Optional[ProductDict]: Product entity data or None - if was not found. - - """ - con = get_server_api_connection() - return con.get_product_by_id( - project_name=project_name, - product_id=product_id, + has_links=has_links, fields=fields, own_attributes=own_attributes, ) -def get_product_by_name( +def get_workfile_info( project_name: str, - product_name: str, - folder_id: str, + task_id: str, + path: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: - """Query product entity by name and folder id. - - Args: - project_name (str): Name of project where to look for queried - entities. - product_name (str): Product name. - folder_id (str): Folder id (Folder is a parent of products). - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - products. - - Returns: - Optional[ProductDict]: Product entity data or None - if was not found. - - """ - con = get_server_api_connection() - return con.get_product_by_name( - project_name=project_name, - product_name=product_name, - folder_id=folder_id, - fields=fields, - own_attributes=own_attributes, - ) - - -def get_product_types( - fields: Optional[Iterable[str]] = None, -) -> list["ProductTypeDict"]: - """Types of products. - - This is server wide information. Product types have 'name', 'icon' and - 'color'. - - Args: - fields (Optional[Iterable[str]]): Product types fields to query. - - Returns: - list[ProductTypeDict]: Product types information. - - """ - con = get_server_api_connection() - return con.get_product_types( - fields=fields, - ) - - -def get_project_product_types( - project_name: str, - fields: Optional[Iterable[str]] = None, -) -> list["ProductTypeDict"]: - """DEPRECATED Types of products available in a project. - - Filter only product types available in a project. +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by task id and workfile path. - Args: - project_name (str): Name of the project where to look for - product types. - fields (Optional[Iterable[str]]): Product types fields to query. + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. Returns: - list[ProductTypeDict]: Product types information. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_project_product_types( + return con.get_workfile_info( project_name=project_name, + task_id=task_id, + path=path, fields=fields, + own_attributes=own_attributes, ) -def get_product_type_names( - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, -) -> set[str]: - """DEPRECATED Product type names. - - Warnings: - This function will be probably removed. Matters if 'products_id' - filter has real use-case. +def get_workfile_info_by_id( + project_name: str, + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional["WorkfileInfoDict"]: + """Workfile info entity by id. Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. Returns: - set[str]: Product type names. + Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_product_type_names( + return con.get_workfile_info_by_id( project_name=project_name, - product_ids=product_ids, + workfile_id=workfile_id, + fields=fields, + own_attributes=own_attributes, ) -def create_product( - project_name: str, - name: str, - product_type: str, - folder_id: str, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - product_id: Optional[str] = None, +def get_full_link_type_name( + link_type_name: str, + input_type: str, + output_type: str, ) -> str: - """Create new product. + """Calculate full link type name used for query from server. Args: - project_name (str): Project name. - name (str): Product name. - product_type (str): Product type. - folder_id (str): Parent folder id. - attrib (Optional[dict[str, Any]]): Product attributes. - data (Optional[dict[str, Any]]): Product data. - tags (Optional[Iterable[str]]): Product tags. - status (Optional[str]): Product status. - active (Optional[bool]): Product active state. - product_id (Optional[str]): Product id. If not passed new id is - generated. + link_type_name (str): Type of link. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. Returns: - str: Product id. + str: Full name of link type used for query from server. """ con = get_server_api_connection() - return con.create_product( - project_name=project_name, - name=name, - product_type=product_type, - folder_id=folder_id, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - product_id=product_id, + return con.get_full_link_type_name( + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def update_product( +def get_link_types( project_name: str, - product_id: str, - name: Optional[str] = None, - folder_id: Optional[str] = None, - product_type: Optional[str] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, -): - """Update product entity on server. - - Update of ``data`` will override existing value on folder entity. +) -> list[dict[str, Any]]: + """All link types available on a project. - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + Example output: + [ + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } + ] Args: - project_name (str): Project name. - product_id (str): Product id. - name (Optional[str]): New product name. - folder_id (Optional[str]): New product id. - product_type (Optional[str]): New product type. - attrib (Optional[dict[str, Any]]): New product attributes. - data (Optional[dict[str, Any]]): New product data. - tags (Optional[Iterable[str]]): New product tags. - status (Optional[str]): New product status. - active (Optional[bool]): New product active state. - - """ - con = get_server_api_connection() - return con.update_product( - project_name=project_name, - product_id=product_id, - name=name, - folder_id=folder_id, - product_type=product_type, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - ) - - -def delete_product( - project_name: str, - product_id: str, -): - """Delete product. + project_name (str): Name of project where to look for link types. - Args: - project_name (str): Project name. - product_id (str): Product id to delete. + Returns: + list[dict[str, Any]]: Link types available on project. """ con = get_server_api_connection() - return con.delete_product( + return con.get_link_types( project_name=project_name, - product_id=product_id, ) -def get_rest_version( +def get_link_type( project_name: str, - version_id: str, -) -> Optional["VersionDict"]: - con = get_server_api_connection() - return con.get_rest_version( - project_name=project_name, - version_id=version_id, - ) + link_type_name: str, + input_type: str, + output_type: str, +) -> Optional[dict[str, Any]]: + """Get link type data. + There is not dedicated REST endpoint to get single link type, + so method 'get_link_types' is used. -def get_versions( - project_name: str, - version_ids: Optional[Iterable[str]] = None, - product_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - versions: Optional[Iterable[str]] = None, - hero: bool = True, - standard: bool = True, - latest: Optional[bool] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Get version entities based on passed filters from server. + Example output: + { + "name": "reference|folder|folder", + "link_type": "reference", + "input_type": "folder", + "output_type": "folder", + "data": {} + } Args: - project_name (str): Name of project where to look for versions. - version_ids (Optional[Iterable[str]]): Version ids used for - version filtering. - product_ids (Optional[Iterable[str]]): Product ids used for - version filtering. - task_ids (Optional[Iterable[str]]): Task ids used for - version filtering. - versions (Optional[Iterable[int]]): Versions we're interested in. - hero (Optional[bool]): Skip hero versions when set to False. - standard (Optional[bool]): Skip standard (non-hero) when - set to False. - latest (Optional[bool]): Return only latest version of standard - versions. This can be combined only with 'standard' attribute - set to True. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields to be queried - for version. All possible folder fields are returned - if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Generator[VersionDict, None, None]: Queried version entities. - - """ - con = get_server_api_connection() - return con.get_versions( - project_name=project_name, - version_ids=version_ids, - product_ids=product_ids, - task_ids=task_ids, - versions=versions, - hero=hero, - standard=standard, - latest=latest, - statuses=statuses, - tags=tags, - active=active, - fields=fields, - own_attributes=own_attributes, + project_name (str): Project where link type is available. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + + Returns: + Optional[dict[str, Any]]: Link type information. + + """ + con = get_server_api_connection() + return con.get_link_type( + project_name=project_name, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_version_by_id( +def create_link_type( project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by id. + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, +): + """Create or update link type on server. + + Warning: + Because PUT is used for creation it is also used for update. Args: - project_name (str): Name of project where to look for queried - entities. - version_id (str): Version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Additional data related to link. - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_version_by_id( + return con.create_link_type( project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def get_version_by_name( +def delete_link_type( project_name: str, - version: int, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query version entity by version and product id. + link_type_name: str, + input_type: str, + output_type: str, +): + """Remove link type from project. Args: - project_name (str): Name of project where to look for queried - entities. - version (int): Version of version entity. - product_id (str): Product id. Product is a parent of version. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where link type is created. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_version_by_name( + return con.delete_link_type( project_name=project_name, - version=version, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, ) -def get_hero_version_by_id( +def make_sure_link_type_exists( project_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by id. + link_type_name: str, + input_type: str, + output_type: str, + data: Optional[dict[str, Any]] = None, +): + """Make sure link type exists on a project. Args: - project_name (str): Name of project where to look for queried - entities. - version_id (int): Hero version id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. - - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + project_name (str): Name of project. + link_type_name (str): Name of link type. + input_type (str): Input entity type of link. + output_type (str): Output entity type of link. + data (Optional[dict[str, Any]]): Link type related data. """ con = get_server_api_connection() - return con.get_hero_version_by_id( + return con.make_sure_link_type_exists( project_name=project_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_type=input_type, + output_type=output_type, + data=data, ) -def get_hero_version_by_product_id( +def create_link( project_name: str, - product_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query hero version entity by product id. + link_type_name: str, + input_id: str, + input_type: str, + output_id: str, + output_type: str, + link_name: Optional[str] = None, +): + """Create link between 2 entities. - Only one hero version is available on a product. + Link has a type which must already exists on a project. + + Example output:: + + { + "id": "59a212c0d2e211eda0e20242ac120002" + } Args: - project_name (str): Name of project where to look for queried - entities. - product_id (int): Product id. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where the link is created. + link_type_name (str): Type of link. + input_id (str): Input entity id. + input_type (str): Entity type of input entity. + output_id (str): Output entity id. + output_type (str): Entity type of output entity. + link_name (Optional[str]): Name of link. + Available from server version '1.0.0-rc.6'. Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + dict[str, str]: Information about link. + + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_hero_version_by_product_id( + return con.create_link( project_name=project_name, - product_id=product_id, - fields=fields, - own_attributes=own_attributes, + link_type_name=link_type_name, + input_id=input_id, + input_type=input_type, + output_id=output_id, + output_type=output_type, + link_name=link_name, ) -def get_hero_versions( +def delete_link( project_name: str, - product_ids: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: - """Query hero versions by multiple filters. - - Only one hero version is available on a product. + link_id: str, +): + """Remove link by id. Args: - project_name (str): Name of project where to look for queried - entities. - product_ids (Optional[Iterable[str]]): Product ids. - version_ids (Optional[Iterable[str]]): Version ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): Fields that should be returned. - All fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where link exists. + link_id (str): Id of link. - Returns: - Optional[VersionDict]: Version entity data or None - if was not found. + Raises: + HTTPRequestError: Server error happened. """ con = get_server_api_connection() - return con.get_hero_versions( + return con.delete_link( project_name=project_name, - product_ids=product_ids, - version_ids=version_ids, - active=active, - fields=fields, - own_attributes=own_attributes, + link_id=link_id, ) -def get_last_versions( +def get_entities_links( project_name: str, - product_ids: Iterable[str], - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> dict[str, Optional["VersionDict"]]: - """Query last version entities by product ids. + entity_type: str, + entity_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, + link_names: Optional[Iterable[str]] = None, + link_name_regex: Optional[str] = None, +) -> dict[str, list[dict[str, Any]]]: + """Helper method to get links from server for entity types. + + .. highlight:: text + .. code-block:: text + + Example output: + { + "59a212c0d2e211eda0e20242ac120001": [ + { + "id": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference", + "description": "reference link between folders", + "projectName": "my_project", + "author": "frantadmin", + "entityId": "b1df109676db11ed8e8c6c9466b19aa8", + "entityType": "folder", + "direction": "out" + }, + ... + ], + ... + } Args: - project_name (str): Project where to look for representation. - product_ids (Iterable[str]): Product ids. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where links are. + entity_type (Literal["folder", "task", "product", + "version", "representations"]): Entity type. + entity_ids (Optional[Iterable[str]]): Ids of entities for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. Returns: - dict[str, Optional[VersionDict]]: Last versions by product id. + dict[str, list[dict[str, Any]]]: Link info by entity ids. """ con = get_server_api_connection() - return con.get_last_versions( + return con.get_entities_links( project_name=project_name, - product_ids=product_ids, - active=active, - fields=fields, - own_attributes=own_attributes, + entity_type=entity_type, + entity_ids=entity_ids, + link_types=link_types, + link_direction=link_direction, + link_names=link_names, + link_name_regex=link_name_regex, ) -def get_last_version_by_product_id( +def get_folders_links( project_name: str, - product_id: str, - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product id. + folder_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query folders links from server. Args: - project_name (str): Project where to look for representation. - product_id (str): Product id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - versions. + project_name (str): Project where links are. + folder_ids (Optional[Iterable[str]]): Ids of folders for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - Optional[VersionDict]: Queried version entity or None. + dict[str, list[dict[str, Any]]]: Link info by folder ids. """ con = get_server_api_connection() - return con.get_last_version_by_product_id( + return con.get_folders_links( project_name=project_name, - product_id=product_id, - active=active, - fields=fields, - own_attributes=own_attributes, + folder_ids=folder_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_last_version_by_product_name( +def get_folder_links( project_name: str, - product_name: str, folder_id: str, - active: Optional[bool] = True, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: - """Query last version entity by product name and folder id. + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query folder links from server. Args: - project_name (str): Project where to look for representation. - product_name (str): Product name. - folder_id (str): Folder id. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (str): Project where links are. + folder_id (str): Folder id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - Optional[VersionDict]: Queried version entity or None. + list[dict[str, Any]]: Link info of folder. """ con = get_server_api_connection() - return con.get_last_version_by_product_name( + return con.get_folder_links( project_name=project_name, - product_name=product_name, folder_id=folder_id, - active=active, - fields=fields, - own_attributes=own_attributes, + link_types=link_types, + link_direction=link_direction, ) -def version_is_latest( +def get_tasks_links( project_name: str, - version_id: str, -) -> bool: - """Is version latest from a product. + task_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query tasks links from server. Args: - project_name (str): Project where to look for representation. - version_id (str): Version id. + project_name (str): Project where links are. + task_ids (Optional[Iterable[str]]): Ids of tasks for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - bool: Version is latest or not. + dict[str, list[dict[str, Any]]]: Link info by task ids. """ con = get_server_api_connection() - return con.version_is_latest( + return con.get_tasks_links( project_name=project_name, - version_id=version_id, + task_ids=task_ids, + link_types=link_types, + link_direction=link_direction, ) -def create_version( +def get_task_links( project_name: str, - version: int, - product_id: str, - task_id: Optional[str] = None, - author: Optional[str] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = None, - version_id: Optional[str] = None, -) -> str: - """Create new version. + task_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query task links from server. Args: - project_name (str): Project name. - version (int): Version. - product_id (str): Parent product id. - task_id (Optional[str]): Parent task id. - author (Optional[str]): Version author. - attrib (Optional[dict[str, Any]]): Version attributes. - data (Optional[dict[str, Any]]): Version data. - tags (Optional[Iterable[str]]): Version tags. - status (Optional[str]): Version status. - active (Optional[bool]): Version active state. - thumbnail_id (Optional[str]): Version thumbnail id. - version_id (Optional[str]): Version id. If not passed new id is - generated. + project_name (str): Project where links are. + task_id (str): Task id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - str: Version id. + list[dict[str, Any]]: Link info of task. """ con = get_server_api_connection() - return con.create_version( + return con.get_task_links( project_name=project_name, - version=version, - product_id=product_id, task_id=task_id, - author=author, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, - version_id=version_id, + link_types=link_types, + link_direction=link_direction, ) -def update_version( +def get_products_links( project_name: str, - version_id: str, - version: Optional[int] = None, - product_id: Optional[str] = None, - task_id: Optional[str] = NOT_SET, - author: Optional[str] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - tags: Optional[Iterable[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - thumbnail_id: Optional[str] = NOT_SET, -): - """Update version entity on server. - - Do not pass ``task_id`` amd ``thumbnail_id`` if you don't - want to change their values. Value ``None`` would unset - their value. - - Update of ``data`` will override existing value on folder entity. - - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + product_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query products links from server. Args: - project_name (str): Project name. - version_id (str): Version id. - version (Optional[int]): New version. - product_id (Optional[str]): New product id. - task_id (Optional[str]): New task id. - author (Optional[str]): New author username. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. - thumbnail_id (Optional[str]): New thumbnail id. + project_name (str): Project where links are. + product_ids (Optional[Iterable[str]]): Ids of products for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + dict[str, list[dict[str, Any]]]: Link info by product ids. """ con = get_server_api_connection() - return con.update_version( - project_name=project_name, - version_id=version_id, - version=version, - product_id=product_id, - task_id=task_id, - author=author, - attrib=attrib, - data=data, - tags=tags, - status=status, - active=active, - thumbnail_id=thumbnail_id, + return con.get_products_links( + project_name=project_name, + product_ids=product_ids, + link_types=link_types, + link_direction=link_direction, ) -def delete_version( +def get_product_links( project_name: str, - version_id: str, -): - """Delete version. + product_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query product links from server. Args: - project_name (str): Project name. - version_id (str): Version id to delete. + project_name (str): Project where links are. + product_id (str): Product id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. + + Returns: + list[dict[str, Any]]: Link info of product. """ con = get_server_api_connection() - return con.delete_version( + return con.get_product_links( project_name=project_name, - version_id=version_id, + product_id=product_id, + link_types=link_types, + link_direction=link_direction, ) -def get_thumbnail_by_id( +def get_versions_links( project_name: str, - thumbnail_id: str, -) -> ThumbnailContent: - """Get thumbnail from server by id. - - Warnings: - Please keep in mind that used endpoint is allowed only for admins - and managers. Use 'get_thumbnail' with entity type and id - to allow access for artists. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. + version_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query versions links from server. Args: - project_name (str): Project under which the entity is located. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + project_name (str): Project where links are. + version_ids (Optional[Iterable[str]]): Ids of versions for which + links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, list[dict[str, Any]]]: Link info by version ids. """ con = get_server_api_connection() - return con.get_thumbnail_by_id( + return con.get_versions_links( project_name=project_name, - thumbnail_id=thumbnail_id, + version_ids=version_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_thumbnail( +def get_version_links( project_name: str, - entity_type: str, - entity_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Get thumbnail from server. - - Permissions of thumbnails are related to entities so thumbnails must - be queried per entity. So an entity type and entity id is required - to be passed. - - Notes: - It is recommended to use one of prepared entity type specific - methods 'get_folder_thumbnail', 'get_version_thumbnail' or - 'get_workfile_thumbnail'. - We do recommend pass thumbnail id if you have access to it. Each - entity that allows thumbnails has 'thumbnailId' field, so it - can be queried. + version_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query version links from server. Args: - project_name (str): Project under which the entity is located. - entity_type (str): Entity type which passed entity id represents. - entity_id (str): Entity id for which thumbnail should be returned. - thumbnail_id (Optional[str]): DEPRECATED Use - 'get_thumbnail_by_id'. + project_name (str): Project where links are. + version_id (str): Version id for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[dict[str, Any]]: Link info of version. """ con = get_server_api_connection() - return con.get_thumbnail( + return con.get_version_links( project_name=project_name, - entity_type=entity_type, - entity_id=entity_id, - thumbnail_id=thumbnail_id, + version_id=version_id, + link_types=link_types, + link_direction=link_direction, ) -def get_folder_thumbnail( +def get_representations_links( project_name: str, - folder_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for folder entity. + representation_ids: Optional[Iterable[str]] = None, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> dict[str, list[dict[str, Any]]]: + """Query representations links from server. Args: - project_name (str): Project under which the entity is located. - folder_id (str): Folder id for which thumbnail should be returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project where links are. + representation_ids (Optional[Iterable[str]]): Ids of + representations for which links should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + dict[str, list[dict[str, Any]]]: Link info by representation ids. """ con = get_server_api_connection() - return con.get_folder_thumbnail( + return con.get_representations_links( project_name=project_name, - folder_id=folder_id, - thumbnail_id=thumbnail_id, + representation_ids=representation_ids, + link_types=link_types, + link_direction=link_direction, ) -def get_task_thumbnail( +def get_representation_links( project_name: str, - task_id: str, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for task entity. + representation_id: str, + link_types: Optional[Iterable[str]] = None, + link_direction: Optional["LinkDirection"] = None, +) -> list[dict[str, Any]]: + """Query representation links from server. Args: - project_name (str): Project under which the entity is located. - task_id (str): Folder id for which thumbnail should be returned. + project_name (str): Project where links are. + representation_id (str): Representation id for which links + should be received. + link_types (Optional[Iterable[str]]): Link type filters. + link_direction (Optional[Literal["in", "out"]]): Link direction + filter. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + list[dict[str, Any]]: Link info of representation. """ con = get_server_api_connection() - return con.get_task_thumbnail( + return con.get_representation_links( project_name=project_name, - task_id=task_id, + representation_id=representation_id, + link_types=link_types, + link_direction=link_direction, ) -def get_version_thumbnail( +def get_entity_lists( project_name: str, - version_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for version entity. + *, + list_ids: Optional[Iterable[str]] = None, + active: Optional[bool] = None, + fields: Optional[Iterable[str]] = None, +) -> Generator[dict[str, Any], None, None]: + """Fetch entity lists from server. Args: - project_name (str): Project under which the entity is located. - version_id (str): Version id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project name where entity lists are. + list_ids (Optional[Iterable[str]]): List of entity list ids to + fetch. + active (Optional[bool]): Filter by active state of entity lists. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + Generator[dict[str, Any], None, None]: Entity list entities + matching defined filters. """ con = get_server_api_connection() - return con.get_version_thumbnail( + return con.get_entity_lists( project_name=project_name, - version_id=version_id, - thumbnail_id=thumbnail_id, + list_ids=list_ids, + active=active, + fields=fields, ) -def get_workfile_thumbnail( +def get_entity_list_rest( project_name: str, - workfile_id: str, - thumbnail_id: Optional[str] = None, -) -> ThumbnailContent: - """Prepared method to receive thumbnail for workfile entity. + list_id: str, +) -> Optional[dict[str, Any]]: + """Get entity list by id using REST API. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Worfile id for which thumbnail should be - returned. - thumbnail_id (Optional[str]): Prepared thumbnail id from entity. - Used only to check if thumbnail was already cached. + project_name (str): Project name. + list_id (str): Entity list id. Returns: - ThumbnailContent: Thumbnail content wrapper. Does not have to be - valid. + Optional[dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.get_workfile_thumbnail( + return con.get_entity_list_rest( project_name=project_name, - workfile_id=workfile_id, - thumbnail_id=thumbnail_id, + list_id=list_id, ) -def create_thumbnail( +def get_entity_list_by_id( project_name: str, - src_filepath: str, - thumbnail_id: Optional[str] = None, -) -> str: - """Create new thumbnail on server from passed path. + list_id: str, + fields: Optional[Iterable[str]] = None, +) -> Optional[dict[str, Any]]: + """Get entity list by id using GraphQl. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - src_filepath (str): Filepath to thumbnail which should be uploaded. - thumbnail_id (Optional[str]): Prepared if of thumbnail. + project_name (str): Project name. + list_id (str): Entity list id. + fields (Optional[Iterable[str]]): Fields to fetch from server. Returns: - str: Created thumbnail id. - - Raises: - ValueError: When thumbnail source cannot be processed. + Optional[dict[str, Any]]: Entity list data or None if not found. """ con = get_server_api_connection() - return con.create_thumbnail( + return con.get_entity_list_by_id( project_name=project_name, - src_filepath=src_filepath, - thumbnail_id=thumbnail_id, + list_id=list_id, + fields=fields, ) -def update_thumbnail( +def create_entity_list( project_name: str, - thumbnail_id: str, - src_filepath: str, -): - """Change thumbnail content by id. - - Update can be also used to create new thumbnail. + entity_type: "EntityListEntityType", + label: str, + *, + list_type: Optional[str] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, + template: Optional[dict[str, Any]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, + items: Optional[list[dict[str, Any]]] = None, + list_id: Optional[str] = None, +) -> str: + """Create entity list. Args: - project_name (str): Project where the thumbnail will be created - and can be used. - thumbnail_id (str): Thumbnail id to update. - src_filepath (str): Filepath to thumbnail which should be uploaded. - - Raises: - ValueError: When thumbnail source cannot be processed. + project_name (str): Project name where entity list lives. + entity_type (EntityListEntityType): Which entity types can be + used in list. + label (str): Entity list label. + list_type (Optional[str]): Entity list type. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + template (Optional[dict[str, Any]]): Dynamic list template. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. + items (Optional[list[dict[str, Any]]]): Initial items in + entity list. + list_id (Optional[str]): Entity list id. """ con = get_server_api_connection() - return con.update_thumbnail( + return con.create_entity_list( project_name=project_name, - thumbnail_id=thumbnail_id, - src_filepath=src_filepath, + entity_type=entity_type, + label=label, + list_type=list_type, + access=access, + attrib=attrib, + data=data, + tags=tags, + template=template, + owner=owner, + active=active, + items=items, + list_id=list_id, ) -def get_workfiles_info( +def update_entity_list( project_name: str, - workfile_ids: Optional[Iterable[str]] = None, - task_ids: Optional[Iterable[str]] = None, - paths: Optional[Iterable[str]] = None, - path_regex: Optional[str] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> "Generator[WorkfileInfoDict, None, None]": - """Workfile info entities by passed filters. + list_id: str, + *, + label: Optional[str] = None, + access: Optional[dict[str, Any]] = None, + attrib: Optional[list[dict[str, Any]]] = None, + data: Optional[list[dict[str, Any]]] = None, + tags: Optional[list[str]] = None, + owner: Optional[str] = None, + active: Optional[bool] = None, +) -> None: + """Update entity list. Args: - project_name (str): Project under which the entity is located. - workfile_ids (Optional[Iterable[str]]): Workfile ids. - task_ids (Optional[Iterable[str]]): Task ids. - paths (Optional[Iterable[str]]): Rootless workfiles paths. - path_regex (Optional[str]): Regex filter for workfile path. - statuses (Optional[Iterable[str]]): Workfile info statuses used - for filtering. - tags (Optional[Iterable[str]]): Workfile info tags used - for filtering. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Generator[WorkfileInfoDict, None, None]: Queried workfile info - entites. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id that will be updated. + label (Optional[str]): New label of entity list. + access (Optional[dict[str, Any]]): Access control for entity list. + attrib (Optional[dict[str, Any]]): Attribute values of + entity list. + data (Optional[dict[str, Any]]): Custom data of entity list. + tags (Optional[list[str]]): Entity list tags. + owner (Optional[str]): New owner of the list. + active (Optional[bool]): Change active state of entity list. """ con = get_server_api_connection() - return con.get_workfiles_info( + return con.update_entity_list( project_name=project_name, - workfile_ids=workfile_ids, - task_ids=task_ids, - paths=paths, - path_regex=path_regex, - statuses=statuses, + list_id=list_id, + label=label, + access=access, + attrib=attrib, + data=data, tags=tags, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, + owner=owner, + active=active, ) -def get_workfile_info( +def delete_entity_list( project_name: str, - task_id: str, - path: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by task id and workfile path. + list_id: str, +) -> None: + """Delete entity list from project. Args: - project_name (str): Project under which the entity is located. - task_id (str): Task id. - path (str): Rootless workfile path. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. - - Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + project_name (str): Project name. + list_id (str): Entity list id that will be removed. """ con = get_server_api_connection() - return con.get_workfile_info( + return con.delete_entity_list( project_name=project_name, - task_id=task_id, - path=path, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, ) -def get_workfile_info_by_id( +def get_entity_list_attribute_definitions( project_name: str, - workfile_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: - """Workfile info entity by id. + list_id: str, +) -> list["EntityListAttributeDefinitionDict"]: + """Get attribute definitioins on entity list. Args: - project_name (str): Project under which the entity is located. - workfile_id (str): Workfile info id. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. + project_name (str): Project name. + list_id (str): Entity list id. Returns: - Optional[WorkfileInfoDict]: Workfile info entity or None. + list[EntityListAttributeDefinitionDict]: List of attribute + definitions. """ con = get_server_api_connection() - return con.get_workfile_info_by_id( + return con.get_entity_list_attribute_definitions( project_name=project_name, - workfile_id=workfile_id, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, ) -def get_rest_representation( +def set_entity_list_attribute_definitions( project_name: str, - representation_id: str, -) -> Optional["RepresentationDict"]: + list_id: str, + attribute_definitions: list["EntityListAttributeDefinitionDict"], +) -> None: + """Set attribute definitioins on entity list. + + Args: + project_name (str): Project name. + list_id (str): Entity list id. + attribute_definitions (list[EntityListAttributeDefinitionDict]): + List of attribute definitions. + + """ con = get_server_api_connection() - return con.get_rest_representation( + return con.set_entity_list_attribute_definitions( project_name=project_name, - representation_id=representation_id, + list_id=list_id, + attribute_definitions=attribute_definitions, ) -def get_representations( +def create_entity_list_item( project_name: str, - representation_ids: Optional[Iterable[str]] = None, - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, - names_by_version_ids: Optional[dict[str, Iterable[str]]] = None, - statuses: Optional[Iterable[str]] = None, - tags: Optional[Iterable[str]] = None, - active: Optional[bool] = True, - has_links: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Generator["RepresentationDict", None, None]: - """Get representation entities based on passed filters from server. - - .. todo:: - - Add separated function for 'names_by_version_ids' filtering. - Because can't be combined with others. + list_id: str, + *, + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + item_id: Optional[str] = None, +) -> str: + """Create entity list item. Args: - project_name (str): Name of project where to look for versions. - representation_ids (Optional[Iterable[str]]): Representation ids - used for representation filtering. - representation_names (Optional[Iterable[str]]): Representation - names used for representation filtering. - version_ids (Optional[Iterable[str]]): Version ids used for - representation filtering. Versions are parents of - representations. - names_by_version_ids (Optional[dict[str, Iterable[str]]]): Find - representations by names and version ids. This filter - discards all other filters. - statuses (Optional[Iterable[str]]): Representation statuses used - for filtering. - tags (Optional[Iterable[str]]): Representation tags used - for filtering. - active (Optional[bool]): Receive active/inactive entities. - Both are returned when 'None' is passed. - has_links (Optional[Literal[IN, OUT, ANY]]): Filter - representations with IN/OUT/ANY links. - fields (Optional[Iterable[str]]): Fields to be queried for - representation. All possible fields are returned if 'None' is - passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (str): Project name where entity list lives. + list_id (str): Entity list id where item will be added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Item attribute values. + data (Optional[dict[str, Any]]): Item data. + tags (Optional[list[str]]): Tags of item in entity list. + item_id (Optional[str]): Id of item that will be created. Returns: - Generator[RepresentationDict, None, None]: Queried - representation entities. + str: Item id. """ con = get_server_api_connection() - return con.get_representations( + return con.create_entity_list_item( project_name=project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - names_by_version_ids=names_by_version_ids, - statuses=statuses, + list_id=list_id, + position=position, + label=label, + attrib=attrib, + data=data, tags=tags, - active=active, - has_links=has_links, - fields=fields, - own_attributes=own_attributes, + item_id=item_id, ) -def get_representation_by_id( +def update_entity_list_items( project_name: str, - representation_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity from server based on id filter. + list_id: str, + items: list[dict[str, Any]], + mode: "EntityListItemMode", +) -> None: + """Update items in entity list. Args: - project_name (str): Project where to look for representation. - representation_id (str): Id of representation. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. - - Returns: - Optional[RepresentationDict]: Queried representation - entity or None. + project_name (str): Project name where entity list live. + list_id (str): Entity list id. + items (list[dict[str, Any]]): Entity list items. + mode (EntityListItemMode): Mode of items update. """ con = get_server_api_connection() - return con.get_representation_by_id( + return con.update_entity_list_items( project_name=project_name, - representation_id=representation_id, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, + items=items, + mode=mode, ) -def get_representation_by_name( +def update_entity_list_item( project_name: str, - representation_name: str, - version_id: str, - fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: - """Query representation entity by name and version id. + list_id: str, + item_id: str, + *, + new_list_id: Optional[str], + position: Optional[int] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, +) -> None: + """Update item in entity list. Args: - project_name (str): Project where to look for representation. - representation_name (str): Representation name. - version_id (str): Version id. - fields (Optional[Iterable[str]]): fields to be queried - for representations. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - representations. + project_name (str): Project name where entity list live. + list_id (str): Entity list id where item lives. + item_id (str): Item id that will be removed from entity list. + new_list_id (Optional[str]): New entity list id where item will be + added. + position (Optional[int]): Position of item in entity list. + label (Optional[str]): Label of item in entity list. + attrib (Optional[dict[str, Any]]): Attributes of item in entity + list. + data (Optional[dict[str, Any]]): Custom data of item in + entity list. + tags (Optional[list[str]]): Tags of item in entity list. - Returns: - Optional[RepresentationDict]: Queried representation entity - or None. + """ + con = get_server_api_connection() + return con.update_entity_list_item( + project_name=project_name, + list_id=list_id, + item_id=item_id, + new_list_id=new_list_id, + position=position, + label=label, + attrib=attrib, + data=data, + tags=tags, + ) + + +def delete_entity_list_item( + project_name: str, + list_id: str, + item_id: str, +) -> None: + """Delete item from entity list. + + Args: + project_name (str): Project name where entity list live. + list_id (str): Entity list id from which item will be removed. + item_id (str): Item id that will be removed from entity list. """ con = get_server_api_connection() - return con.get_representation_by_name( + return con.delete_entity_list_item( project_name=project_name, - representation_name=representation_name, - version_id=version_id, - fields=fields, - own_attributes=own_attributes, + list_id=list_id, + item_id=item_id, ) -def get_representations_hierarchy( +def get_thumbnail_by_id( project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> dict[str, RepresentationHierarchy]: - """Find representation with parents by representation id. + thumbnail_id: str, +) -> ThumbnailContent: + """Get thumbnail from server by id. - Representation entity with parent entities up to project. + Warnings: + Please keep in mind that used endpoint is allowed only for admins + and managers. Use 'get_thumbnail' with entity type and id + to allow access for artists. - Default fields are used when any fields are set to `None`. But it is - possible to pass in empty iterable (list, set, tuple) to skip - entity. + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + project_name (str): Project under which the entity is located. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. Returns: - dict[str, RepresentationHierarchy]: Parent entities by - representation id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representations_hierarchy( + return con.get_thumbnail_by_id( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, + thumbnail_id=thumbnail_id, ) -def get_representation_hierarchy( +def get_thumbnail( project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - task_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, - representation_fields: Optional[Iterable[str]] = None, -) -> Optional[RepresentationHierarchy]: - """Find representation parents by representation id. + entity_type: str, + entity_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Get thumbnail from server. - Representation parent entities up to project. + Permissions of thumbnails are related to entities so thumbnails must + be queried per entity. So an entity type and entity id is required + to be passed. + + Notes: + It is recommended to use one of prepared entity type specific + methods 'get_folder_thumbnail', 'get_version_thumbnail' or + 'get_workfile_thumbnail'. + We do recommend pass thumbnail id if you have access to it. Each + entity that allows thumbnails has 'thumbnailId' field, so it + can be queried. Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - task_fields (Optional[Iterable[str]]): Task fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. - representation_fields (Optional[Iterable[str]]): Representation - fields. + project_name (str): Project under which the entity is located. + entity_type (str): Entity type which passed entity id represents. + entity_id (str): Entity id for which thumbnail should be returned. + thumbnail_id (Optional[str]): DEPRECATED Use + 'get_thumbnail_by_id'. Returns: - RepresentationHierarchy: Representation hierarchy entities. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representation_hierarchy( + return con.get_thumbnail( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - task_fields=task_fields, - product_fields=product_fields, - version_fields=version_fields, - representation_fields=representation_fields, + entity_type=entity_type, + entity_id=entity_id, + thumbnail_id=thumbnail_id, ) - -def get_representations_parents( - project_name: str, - representation_ids: Iterable[str], - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> dict[str, RepresentationParents]: - """Find representations parents by representation id. - - Representation parent entities up to project. + +def get_folder_thumbnail( + project_name: str, + folder_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for folder entity. Args: - project_name (str): Project where to look for entities. - representation_ids (Iterable[str]): Representation ids. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. + project_name (str): Project under which the entity is located. + folder_id (str): Folder id for which thumbnail should be returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - dict[str, RepresentationParents]: Parent entities by - representation id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representations_parents( + return con.get_folder_thumbnail( project_name=project_name, - representation_ids=representation_ids, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + folder_id=folder_id, + thumbnail_id=thumbnail_id, ) -def get_representation_parents( +def get_task_thumbnail( project_name: str, - representation_id: str, - project_fields: Optional[Iterable[str]] = None, - folder_fields: Optional[Iterable[str]] = None, - product_fields: Optional[Iterable[str]] = None, - version_fields: Optional[Iterable[str]] = None, -) -> Optional["RepresentationParents"]: - """Find representation parents by representation id. - - Representation parent entities up to project. + task_id: str, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for task entity. Args: - project_name (str): Project where to look for entities. - representation_id (str): Representation id. - project_fields (Optional[Iterable[str]]): Project fields. - folder_fields (Optional[Iterable[str]]): Folder fields. - product_fields (Optional[Iterable[str]]): Product fields. - version_fields (Optional[Iterable[str]]): Version fields. + project_name (str): Project under which the entity is located. + task_id (str): Folder id for which thumbnail should be returned. Returns: - RepresentationParents: Representation parent entities. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_representation_parents( + return con.get_task_thumbnail( project_name=project_name, - representation_id=representation_id, - project_fields=project_fields, - folder_fields=folder_fields, - product_fields=product_fields, - version_fields=version_fields, + task_id=task_id, ) -def get_repre_ids_by_context_filters( +def get_version_thumbnail( project_name: str, - context_filters: Optional[dict[str, Iterable[str]]], - representation_names: Optional[Iterable[str]] = None, - version_ids: Optional[Iterable[str]] = None, -) -> list[str]: - """Find representation ids which match passed context filters. - - Each representation has context integrated on representation entity in - database. The context may contain project, folder, task name or - product name, product type and many more. This implementation gives - option to quickly filter representation based on representation data - in database. - - Context filters have defined structure. To define filter of nested - subfield use dot '.' as delimiter (For example 'task.name'). - Filter values can be regex filters. String or ``re.Pattern`` can - be used. + version_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for version entity. Args: - project_name (str): Project where to look for representations. - context_filters (dict[str, list[str]]): Filters of context fields. - representation_names (Optional[Iterable[str]]): Representation - names, can be used as additional filter for representations - by their names. - version_ids (Optional[Iterable[str]]): Version ids, can be used - as additional filter for representations by their parent ids. + project_name (str): Project under which the entity is located. + version_id (str): Version id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - list[str]: Representation ids that match passed filters. - - Example: - The function returns just representation ids so if entities are - required for funtionality they must be queried afterwards by - their ids. - >>> from ayon_api import get_repre_ids_by_context_filters - >>> from ayon_api import get_representations - >>> project_name = "testProject" - >>> filters = { - ... "task.name": ["[aA]nimation"], - ... "product": [".*[Mm]ain"] - ... } - >>> repre_ids = get_repre_ids_by_context_filters( - ... project_name, filters) - >>> repres = get_representations(project_name, repre_ids) + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.get_repre_ids_by_context_filters( + return con.get_version_thumbnail( project_name=project_name, - context_filters=context_filters, - representation_names=representation_names, - version_ids=version_ids, + version_id=version_id, + thumbnail_id=thumbnail_id, ) -def create_representation( +def get_workfile_thumbnail( project_name: str, - name: str, - version_id: str, - files: Optional[list[dict[str, Any]]] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - traits: Optional[dict[str, Any]] = None, - tags: Optional[list[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, - representation_id: Optional[str] = None, -) -> str: - """Create new representation. + workfile_id: str, + thumbnail_id: Optional[str] = None, +) -> ThumbnailContent: + """Prepared method to receive thumbnail for workfile entity. Args: - project_name (str): Project name. - name (str): Representation name. - version_id (str): Parent version id. - files (Optional[list[dict]]): Representation files information. - attrib (Optional[dict[str, Any]]): Representation attributes. - data (Optional[dict[str, Any]]): Representation data. - traits (Optional[dict[str, Any]]): Representation traits - serialized data as dict. - tags (Optional[Iterable[str]]): Representation tags. - status (Optional[str]): Representation status. - active (Optional[bool]): Representation active state. - representation_id (Optional[str]): Representation id. If not - passed new id is generated. + project_name (str): Project under which the entity is located. + workfile_id (str): Worfile id for which thumbnail should be + returned. + thumbnail_id (Optional[str]): Prepared thumbnail id from entity. + Used only to check if thumbnail was already cached. Returns: - str: Representation id. + ThumbnailContent: Thumbnail content wrapper. Does not have to be + valid. """ con = get_server_api_connection() - return con.create_representation( + return con.get_workfile_thumbnail( project_name=project_name, - name=name, - version_id=version_id, - files=files, - attrib=attrib, - data=data, - traits=traits, - tags=tags, - status=status, - active=active, - representation_id=representation_id, + workfile_id=workfile_id, + thumbnail_id=thumbnail_id, ) -def update_representation( +def create_thumbnail( project_name: str, - representation_id: str, - name: Optional[str] = None, - version_id: Optional[str] = None, - files: Optional[list[dict[str, Any]]] = None, - attrib: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = None, - traits: Optional[dict[str, Any]] = None, - tags: Optional[list[str]] = None, - status: Optional[str] = None, - active: Optional[bool] = None, -): - """Update representation entity on server. + src_filepath: str, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. - Update of ``data`` will override existing value on folder entity. + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + src_filepath (str): Filepath to thumbnail which should be uploaded. + thumbnail_id (Optional[str]): Prepared if of thumbnail. - Update of ``attrib`` does change only passed attributes. If you want - to unset value, use ``None``. + Returns: + str: Created thumbnail id. - Args: - project_name (str): Project name. - representation_id (str): Representation id. - name (Optional[str]): New name. - version_id (Optional[str]): New version id. - files (Optional[list[dict]]): New files - information. - attrib (Optional[dict[str, Any]]): New attributes. - data (Optional[dict[str, Any]]): New data. - traits (Optional[dict[str, Any]]): New traits. - tags (Optional[Iterable[str]]): New tags. - status (Optional[str]): New status. - active (Optional[bool]): New active state. + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.update_representation( + return con.create_thumbnail( project_name=project_name, - representation_id=representation_id, - name=name, - version_id=version_id, - files=files, - attrib=attrib, - data=data, - traits=traits, - tags=tags, - status=status, - active=active, + src_filepath=src_filepath, + thumbnail_id=thumbnail_id, ) -def delete_representation( +def update_thumbnail( project_name: str, - representation_id: str, + thumbnail_id: str, + src_filepath: str, ): - """Delete representation. + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. Args: - project_name (str): Project name. - representation_id (str): Representation id to delete. + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + src_filepath (str): Filepath to thumbnail which should be uploaded. + + Raises: + ValueError: When thumbnail source cannot be processed. """ con = get_server_api_connection() - return con.delete_representation( + return con.update_thumbnail( project_name=project_name, - representation_id=representation_id, + thumbnail_id=thumbnail_id, + src_filepath=src_filepath, ) From d9994be92d42d2c87dccf6ce9f319552d7510ae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:08:52 +0200 Subject: [PATCH 157/506] ruff fixes --- ayon_api/_api.py | 10 ++++++---- ayon_api/_api_helpers/actions.py | 6 +++--- ayon_api/_api_helpers/events.py | 2 +- ayon_api/_api_helpers/folders.py | 2 +- ayon_api/_api_helpers/links.py | 2 +- ayon_api/_api_helpers/projects.py | 2 +- ayon_api/_api_helpers/tasks.py | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 475396d29..4ef2be426 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -12,7 +12,8 @@ import os import socket import typing -from typing import Optional, Set, List, Tuple, Dict, Iterable, Generator, Any +from typing import Optional, Iterable, Generator, Any + import requests from .constants import ( @@ -40,6 +41,7 @@ from typing import Union from .typing import ( ServerVersion, + ActionManifestDict, ActivityType, ActivityReferenceType, EntityListEntityType, @@ -307,7 +309,7 @@ def get_service_addon_settings(project_name=None): project_name (Optional[str]): Project name. Returns: - Dict[str, Any]: Addon settings. + dict[str, Any]: Addon settings. Raises: ValueError: When service was not initialized. @@ -1688,7 +1690,7 @@ def get_actions( *, variant: Optional[str] = None, mode: Optional["ActionModeType"] = None, -) -> list["ActionManifestdict"]: +) -> list["ActionManifestDict"]: """Get actions for a context. Args: @@ -1705,7 +1707,7 @@ def get_actions( mode (Optional[ActionModeType]): Action modes. Returns: - list[ActionManifestdict]: list of action manifests. + list[ActionManifestDict]: list of action manifests. """ con = get_server_api_connection() diff --git a/ayon_api/_api_helpers/actions.py b/ayon_api/_api_helpers/actions.py index 6cbe9b7bd..136bf29cb 100644 --- a/ayon_api/_api_helpers/actions.py +++ b/ayon_api/_api_helpers/actions.py @@ -10,7 +10,7 @@ if typing.TYPE_CHECKING: from ayon_api.typing import ( ActionEntityTypes, - ActionManifestdict, + ActionManifestDict, ActionTriggerResponse, ActionTakeResponse, ActionConfigResponse, @@ -30,7 +30,7 @@ def get_actions( *, variant: Optional[str] = None, mode: Optional["ActionModeType"] = None, - ) -> list["ActionManifestdict"]: + ) -> list["ActionManifestDict"]: """Get actions for a context. Args: @@ -47,7 +47,7 @@ def get_actions( mode (Optional[ActionModeType]): Action modes. Returns: - list[ActionManifestdict]: list of action manifests. + list[ActionManifestDict]: list of action manifests. """ if variant is None: diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index 3c7d61273..f028d5c9e 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -382,4 +382,4 @@ def enroll_event_job( self.log.error(response.text) return None - return response.data \ No newline at end of file + return response.data diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index 7f6b3ca94..a0805b73f 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -635,4 +635,4 @@ def delete_folder( if force: url += "?force=true" response = self.delete(url) - response.raise_for_status() \ No newline at end of file + response.raise_for_status() diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index 7e75db966..f0a5f6580 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -654,4 +654,4 @@ def get_representation_links( """ return self.get_representations_links( project_name, [representation_id], link_types, link_direction - )[representation_id] \ No newline at end of file + )[representation_id] diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index f24a67f3c..c54340458 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -734,4 +734,4 @@ def _get_project_roots_values( f"projects/{project_name}/siteRoots{query}" ) response.raise_for_status() - return response.data \ No newline at end of file + return response.data diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index 0e825de01..7be323c30 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -512,4 +512,4 @@ def delete_task(self, project_name: str, task_id: str): response = self.delete( f"projects/{project_name}/tasks/{task_id}" ) - response.raise_for_status() \ No newline at end of file + response.raise_for_status() From d6b112c0ea1aaf79cbeb5b87eead22159718a460 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:09:28 +0200 Subject: [PATCH 158/506] added annotations import to public api --- ayon_api/_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 4ef2be426..46a04fadb 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -9,6 +9,8 @@ automatically, and changing them manually can cause issues. """ +from __future__ import annotations + import os import socket import typing From 8a2571e4a8ce6fc341544655c4dd3697dbe04cfc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 Aug 2025 12:17:14 +0200 Subject: [PATCH 159/506] more ruff fixes --- ayon_api/_api.py | 1 - ayon_api/exceptions.py | 8 +++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 46a04fadb..0b44346ed 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -43,7 +43,6 @@ from typing import Union from .typing import ( ServerVersion, - ActionManifestDict, ActivityType, ActivityReferenceType, EntityListEntityType, diff --git a/ayon_api/exceptions.py b/ayon_api/exceptions.py index 55343b1e8..6a44e9d9c 100644 --- a/ayon_api/exceptions.py +++ b/ayon_api/exceptions.py @@ -2,14 +2,16 @@ try: # This should be used if 'requests' have it available - from requests.exceptions import JSONDecodeError as RequestsJSONDecodeError + from requests.exceptions import JSONDecodeError except ImportError: # Older versions of 'requests' don't have custom exception for json # decode error try: - from simplejson import JSONDecodeError as RequestsJSONDecodeError + from simplejson import JSONDecodeError except ImportError: - from json import JSONDecodeError as RequestsJSONDecodeError + from json import JSONDecodeError + +RequestsJSONDecodeError = JSONDecodeError class UrlError(Exception): From 001061da0085e5e3637cbc2bb7a70f3b58e21c14 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Aug 2025 14:14:25 +0200 Subject: [PATCH 160/506] do not handle 'taskId' in nullable arguments --- ayon_api/operations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 8a9dac465..bc3ea457c 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1320,7 +1320,6 @@ def update_version( for key, value in ( ("version", version), ("productId", product_id), - ("taskId", task_id), ("attrib", attrib), ("data", data), ("tags", tags), From 03c16f5a38ac67fac9c8fd10f11225a2881a4099 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:26:27 +0200 Subject: [PATCH 161/506] added type hints to operations logic --- ayon_api/operations.py | 641 ++++++++++++++++++++++------------------- 1 file changed, 344 insertions(+), 297 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 8a9dac465..24c1f507d 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1,14 +1,21 @@ +from __future__ import annotations + import os import copy import collections import uuid from abc import ABC, abstractmethod +import typing +from typing import Optional, Any from ._api import get_server_api_connection from .utils import create_entity_id, REMOVED_VALUE, NOT_SET +if typing.TYPE_CHECKING: + from .server_api import ServerAPI + -def _create_or_convert_to_id(entity_id=None): +def _create_or_convert_to_id(entity_id: Optional[str] = None) -> str: if entity_id is None: return create_entity_id() @@ -17,7 +24,11 @@ def _create_or_convert_to_id(entity_id=None): return entity_id -def prepare_changes(old_entity, new_entity, entity_type): +def prepare_changes( + old_entity: dict[str, Any], + new_entity: dict[str, Any], + entity_type: str, +) -> dict[str, Any]: """Prepare changes for entity update. Notes: @@ -54,16 +65,16 @@ def prepare_changes(old_entity, new_entity, entity_type): def new_folder_entity( - name, - folder_type, - parent_id=None, - status=None, - tags=None, - attribs=None, - data=None, - thumbnail_id=None, - entity_id=None -): + name: str, + folder_type: str, + parent_id: Optional[str] = None, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + thumbnail_id: Optional[str] = None, + entity_id: Optional[str] = None +) -> dict[str, Any]: """Create skeleton data of folder entity. Args: @@ -71,17 +82,17 @@ def new_folder_entity( folder_type (str): Type of folder. parent_id (Optional[str]): Parent folder id. status (Optional[str]): Product status. - tags (Optional[List[str]]): List of tags. - attribs (Optional[Dict[str, Any]]): Explicitly set attributes + tags (Optional[list[str]]): List of tags. + attribs (Optional[dict[str, Any]]): Explicitly set attributes of folder. - data (Optional[Dict[str, Any]]): Custom folder data. Empty dictionary + data (Optional[dict[str, Any]]): Custom folder data. Empty dictionary is used if not passed. thumbnail_id (Optional[str]): Thumbnail id related to folder. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of folder entity. + dict[str, Any]: Skeleton of folder entity. """ if attribs is None: @@ -111,15 +122,15 @@ def new_folder_entity( def new_product_entity( - name, - product_type, - folder_id, - status=None, - tags=None, - attribs=None, - data=None, - entity_id=None -): + name: str, + product_type: str, + folder_id: str, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None, +) -> dict[str, Any]: """Create skeleton data of product entity. Args: @@ -128,16 +139,16 @@ def new_product_entity( product_type (str): Product type. folder_id (str): Parent folder id. status (Optional[str]): Product status. - tags (Optional[List[str]]): List of tags. - attribs (Optional[Dict[str, Any]]): Explicitly set attributes + tags (Optional[list[str]]): List of tags. + attribs (Optional[dict[str, Any]]): Explicitly set attributes of product. - data (Optional[Dict[str, Any]]): product entity data. Empty dictionary + data (Optional[dict[str, Any]]): product entity data. Empty dictionary is used if not passed. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of product entity. + dict[str, Any]: Skeleton of product entity. """ if attribs is None: @@ -162,17 +173,17 @@ def new_product_entity( def new_version_entity( - version, - product_id, - task_id=None, - thumbnail_id=None, - author=None, - status=None, - tags=None, - attribs=None, - data=None, - entity_id=None -): + version: int, + product_id: str, + task_id: Optional[str] = None, + thumbnail_id: Optional[str] = None, + author: Optional[str] = None, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None, +) -> dict[str, Any]: """Create skeleton data of version entity. Args: @@ -183,15 +194,15 @@ def new_version_entity( thumbnail_id (Optional[str]): Thumbnail related to version. author (Optional[str]): Name of version author. status (Optional[str]): Version status. - tags (Optional[List[str]]): List of tags. - attribs (Optional[Dict[str, Any]]): Explicitly set attributes + tags (Optional[list[str]]): List of tags. + attribs (Optional[dict[str, Any]]): Explicitly set attributes of version. - data (Optional[Dict[str, Any]]): Version entity custom data. + data (Optional[dict[str, Any]]): Version entity custom data. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of version entity. + dict[str, Any]: Skeleton of version entity. """ if attribs is None: @@ -221,17 +232,17 @@ def new_version_entity( def new_hero_version_entity( - version, - product_id, - task_id=None, - thumbnail_id=None, - author=None, - status=None, - tags=None, - attribs=None, - data=None, - entity_id=None -): + version: int, + product_id: str, + task_id: Optional[str] = None, + thumbnail_id: Optional[str] = None, + author: Optional[str] = None, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None, +) -> dict[str, Any]: """Create skeleton data of hero version entity. Args: @@ -242,18 +253,17 @@ def new_hero_version_entity( thumbnail_id (Optional[str]): Thumbnail related to version. author (Optional[str]): Name of version author. status (Optional[str]): Version status. - tags (Optional[List[str]]): List of tags. - attribs (Optional[Dict[str, Any]]): Explicitly set attributes + tags (Optional[list[str]]): List of tags. + attribs (Optional[dict[str, Any]]): Explicitly set attributes of version. - data (Optional[Dict[str, Any]]): Version entity data. + data (Optional[dict[str, Any]]): Version entity data. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of version entity. + dict[str, Any]: Skeleton of version entity. """ - return new_version_entity( -abs(int(version)), product_id, @@ -269,16 +279,16 @@ def new_hero_version_entity( def new_representation_entity( - name, - version_id, + name: str, + version_id: str, files, - status=None, - tags=None, - attribs=None, - data=None, - traits=None, - entity_id=None, -): + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None, +) -> dict[str, Any]: """Create skeleton data of representation entity. Args: @@ -287,17 +297,17 @@ def new_representation_entity( version_id (str): Parent version id. files (list[dict[str, str]]): List of files in representation. status (Optional[str]): Representation status. - tags (Optional[List[str]]): List of tags. - attribs (Optional[Dict[str, Any]]): Explicitly set attributes + tags (Optional[list[str]]): List of tags. + attribs (Optional[dict[str, Any]]): Explicitly set attributes of representation. - data (Optional[Dict[str, Any]]): Representation entity data. - traits (Optional[Dict[str, Any]]): Representation traits. Empty + data (Optional[dict[str, Any]]): Representation entity data. + traits (Optional[dict[str, Any]]): Representation traits. Empty if not passed. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of representation entity. + dict[str, Any]: Skeleton of representation entity. """ if attribs is None: @@ -324,15 +334,15 @@ def new_representation_entity( def new_workfile_info( - filepath, - task_id, - status=None, - tags=None, - attribs=None, - description=None, - data=None, - entity_id=None -): + filepath: str, + task_id: str, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + description: Optional[str] = None, + data: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None, +) -> dict[str, Any]: """Create skeleton data of workfile info entity. Workfile entity is at this moment used primarily for artist notes. @@ -341,15 +351,15 @@ def new_workfile_info( filepath (str): Rootless workfile filepath. task_id (str): Task under which was workfile created. status (Optional[str]): Workfile status. - tags (Optional[List[str]]): Workfile tags. + tags (Optional[list[str]]): Workfile tags. attribs (Options[dic[str, Any]]): Explicitly set attributes. description (Optional[str]): Workfile description. - data (Optional[Dict[str, Any]]): Additional metadata. + data (Optional[dict[str, Any]]): Additional metadata. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. Returns: - Dict[str, Any]: Skeleton of workfile info entity. + dict[str, Any]: Skeleton of workfile info entity. """ if attribs is None: @@ -391,36 +401,49 @@ class AbstractOperation(ABC): e.g. 'folder', 'representation' etc. """ - def __init__(self, project_name, entity_type, session): + def __init__( + self, + project_name: str, + entity_type: str, + session: OperationsSession, + ) -> None: self._project_name = project_name self._entity_type = entity_type self._session = session self._id = str(uuid.uuid4()) @property - def project_name(self): + def project_name(self) -> str: return self._project_name @property - def id(self): + def id(self) -> str: """Identifier of operation.""" return self._id @property - def entity_type(self): + def entity_type(self) -> str: return self._entity_type @property @abstractmethod - def operation_name(self): + def operation_name(self) -> str: """Stringified type of operation.""" pass - def to_data(self): + @property + def session(self) -> OperationsSession: + return self._session + + @property + def con(self) -> "ServerAPI": + return self.session.con + + def to_data(self) -> dict[str, Any]: """Convert opration to data that can be converted to json or others. Returns: - Dict[str, Any]: Description of operation. + dict[str, Any]: Description of operation. """ return { @@ -438,12 +461,18 @@ class CreateOperation(AbstractOperation): project_name (str): On which project operation will happen. entity_type (str): Type of entity on which change happens. e.g. 'folder', 'representation' etc. - data (Dict[str, Any]): Data of entity that will be created. + data (dict[str, Any]): Data of entity that will be created. """ operation_name = "create" - def __init__(self, project_name, entity_type, data, session): + def __init__( + self, + project_name: str, + entity_type: str, + data: Optional[dict[str, Any]], + session: OperationsSession, + ) -> None: if not data: data = {} else: @@ -453,44 +482,34 @@ def __init__(self, project_name, entity_type, data, session): data["id"] = create_entity_id() self._data = data - super(CreateOperation, self).__init__( - project_name, entity_type, session - ) + super().__init__(project_name, entity_type, session) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: self.set_value(key, value) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: return self.data[key] - def set_value(self, key, value): + def set_value(self, key: str, value: Any) -> None: self.data[key] = value - def get(self, key, *args, **kwargs): + def get(self, key: str, *args, **kwargs) -> Any: return self.data.get(key, *args, **kwargs) @property - def con(self): - return self.session.con - - @property - def session(self): - return self._session - - @property - def entity_id(self): + def entity_id(self) -> str: return self._data["id"] @property - def data(self): + def data(self) -> dict[str, Any]: return self._data - def to_data(self): - output = super(CreateOperation, self).to_data() + def to_data(self) -> dict[str, Any]: + output = super().to_data() output["data"] = copy.deepcopy(self.data) return output - def to_server_operation(self): + def to_server_operation(self) -> dict[str, Any]: return { "id": self.id, "type": "create", @@ -508,7 +527,7 @@ class UpdateOperation(AbstractOperation): entity_type (str): Type of entity on which change happens. e.g. 'folder', 'representation' etc. entity_id (str): Identifier of an entity. - update_data (Dict[str, Any]): Key -> value changes that will be set in + update_data (dict[str, Any]): Key -> value changes that will be set in database. If value is set to 'REMOVED_VALUE' the key will be removed. Only first level of dictionary is checked (on purpose). @@ -516,46 +535,41 @@ class UpdateOperation(AbstractOperation): operation_name = "update" def __init__( - self, project_name, entity_type, entity_id, update_data, session + self, + project_name: str, + entity_type: str, + entity_id: str, + update_data: dict[str, Any], + session: OperationsSession, ): - super(UpdateOperation, self).__init__( - project_name, entity_type, session - ) + super().__init__(project_name, entity_type, session) self._entity_id = entity_id self._update_data = update_data @property - def entity_id(self): + def entity_id(self) -> str: return self._entity_id @property - def update_data(self): + def update_data(self) -> dict[str, Any]: return self._update_data - @property - def con(self): - return self.session.con - - @property - def session(self): - return self._session - - def to_data(self): + def to_data(self) -> dict[str, Any]: changes = {} for key, value in self._update_data.items(): if value is REMOVED_VALUE: value = None changes[key] = value - output = super(UpdateOperation, self).to_data() + output = super().to_data() output.update({ "entity_id": self.entity_id, "changes": changes }) return output - def to_server_operation(self): + def to_server_operation(self) -> Optional[dict[str, Any]]: if not self._update_data: return None @@ -586,31 +600,27 @@ class DeleteOperation(AbstractOperation): """ operation_name = "delete" - def __init__(self, project_name, entity_type, entity_id, session): + def __init__( + self, + project_name: str, + entity_type: str, + entity_id: str, + session: OperationsSession, + ) -> None: self._entity_id = entity_id - super(DeleteOperation, self).__init__( - project_name, entity_type, session - ) + super().__init__(project_name, entity_type, session) @property - def entity_id(self): + def entity_id(self) -> str: return self._entity_id - @property - def con(self): - return self.session.con - - @property - def session(self): - return self._session - - def to_data(self): - output = super(DeleteOperation, self).to_data() + def to_data(self) -> dict[str, Any]: + output = super().to_data() output["entity_id"] = self.entity_id return output - def to_server_operation(self): + def to_server_operation(self) -> dict[str, Any]: return { "id": self.id, "type": self.operation_name, @@ -634,7 +644,7 @@ class OperationsSession(object): is used if not passed. """ - def __init__(self, con=None): + def __init__(self, con: Optional["ServerApi"] = None) -> None: if con is None: con = get_server_api_connection() self._con = con @@ -643,19 +653,21 @@ def __init__(self, con=None): self._nested_operations = collections.defaultdict(list) @property - def con(self): + def con(self) -> "ServerAPI": return self._con - def get_project(self, project_name): + def get_project( + self, project_name: str + ) -> Optional[dict[str, Any]]: if project_name not in self._project_cache: self._project_cache[project_name] = self.con.get_project( project_name) return copy.deepcopy(self._project_cache[project_name]) - def __len__(self): + def __len__(self) -> int: return len(self._operations) - def add(self, operation): + def add(self, operation: AbstractOperation) -> None: """Add operation to be processed. Args: @@ -672,7 +684,7 @@ def add(self, operation): self._operations.append(operation) - def append(self, operation): + def append(self, operation: AbstractOperation) -> None: """Add operation to be processed. Args: @@ -681,32 +693,32 @@ def append(self, operation): """ self.add(operation) - def extend(self, operations): + def extend(self, operations: list[AbstractOperation]) -> None: """Add operations to be processed. Args: - operations (List[BaseOperation]): Operations that should be + operations (list[BaseOperation]): Operations that should be processed. """ for operation in operations: self.add(operation) - def remove(self, operation): + def remove(self, operation: AbstractOperation) -> None: """Remove operation.""" self._operations.remove(operation) - def clear(self): + def clear(self) -> None: """Clear all registered operations.""" self._operations = [] - def to_data(self): + def to_data(self) -> list[dict[str, Any]]: return [ operation.to_data() for operation in self._operations ] - def commit(self): + def commit(self) -> None: """Commit session operations.""" operations, self._operations = self._operations, [] if not operations: @@ -727,7 +739,13 @@ def commit(self): project_name, operations_body, can_fail=False ) - def create_entity(self, project_name, entity_type, data, nested_id=None): + def create_entity( + self, + project_name: str, + entity_type: str, + data: dict[str, Any], + nested_id: Optional[str] = None, + ) -> CreateOperation: """Fast access to 'CreateOperation'. Args: @@ -756,8 +774,13 @@ def create_entity(self, project_name, entity_type, data, nested_id=None): return operation def update_entity( - self, project_name, entity_type, entity_id, update_data, nested_id=None - ): + self, + project_name: str, + entity_type: str, + entity_id: str, + update_data: dict[str, Any], + nested_id: Optional[str] = None, + ) -> UpdateOperation: """Fast access to 'UpdateOperation'. Returns: @@ -776,8 +799,12 @@ def update_entity( return operation def delete_entity( - self, project_name, entity_type, entity_id, nested_id=None - ): + self, + project_name: str, + entity_type: str, + entity_id: str, + nested_id: Optional[str] = None, + ) -> DeleteOperation: """Fast access to 'DeleteOperation'. Returns: @@ -797,19 +824,19 @@ def delete_entity( def create_folder( self, - project_name, - name, - folder_type=None, - parent_id=None, - label=None, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - thumbnail_id=None, - folder_id=None, - ): + project_name: str, + name: str, + folder_type: Optional[str] = None, + parent_id: Optional[str] = None, + label: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + folder_id: Optional[str] = None, + ) -> CreateOperation: """Create new folder. Args: @@ -858,18 +885,18 @@ def create_folder( def update_folder( self, - project_name, - folder_id, - name=None, - folder_type=None, - parent_id=NOT_SET, - label=NOT_SET, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - thumbnail_id=NOT_SET, + project_name: str, + folder_id: str, + name: Optional[str] = None, + folder_type: Optional[str] = None, + parent_id: Optional[str] = NOT_SET, + label: Optional[str] = NOT_SET, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, ): """Update folder entity on server. @@ -925,7 +952,11 @@ def update_folder( project_name, "folder", folder_id, update_data ) - def delete_folder(self, project_name, folder_id): + def delete_folder( + self, + project_name: str, + folder_id: str, + ) -> DeleteOperation: """Delete folder. Args: @@ -942,20 +973,20 @@ def delete_folder(self, project_name, folder_id): def create_task( self, - project_name, - name, - task_type, - folder_id, - label=None, - assignees=None, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - thumbnail_id=None, - task_id=None, - ): + project_name: str, + name: str, + task_type: str, + folder_id: str, + label: Optional[str] = None, + assignees: Optional[Iterable[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, + ) -> CreateOperation: """Create new task. Args: @@ -1005,20 +1036,20 @@ def create_task( def update_task( self, - project_name, - task_id, - name=None, - task_type=None, - folder_id=None, - label=NOT_SET, - assignees=None, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - thumbnail_id=NOT_SET, - ): + project_name: str, + task_id: str, + name: Optional[str] = None, + task_type: Optional[str] = None, + folder_id: Optional[str] = None, + label: Optional[str] = NOT_SET, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ) -> UpdateOperation: """Update task entity on server. Do not pass ``label`` amd ``thumbnail_id`` if you don't @@ -1075,7 +1106,11 @@ def update_task( project_name, "task", task_id, update_data ) - def delete_task(self, project_name, task_id): + def delete_task( + self, + project_name: str, + task_id: str, + ) -> DeleteOperation: """Delete task. Args: @@ -1090,17 +1125,17 @@ def delete_task(self, project_name, task_id): def create_product( self, - project_name, - name, - product_type, - folder_id, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - product_id=None, - ): + project_name: str, + name: str, + product_type: str, + folder_id: str, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + product_id: Optional[str] = None, + ) -> CreateOperation: """Create new product. Args: @@ -1144,17 +1179,17 @@ def create_product( def update_product( self, - project_name, - product_id, - name=None, - folder_id=None, - product_type=None, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - ): + project_name: str, + product_id: str, + name: Optional[str] = None, + folder_id: Optional[str] = None, + product_type: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ) -> UpdateOperation: """Update product entity on server. Update of ``data`` will override existing value on folder entity. @@ -1199,7 +1234,11 @@ def update_product( update_data ) - def delete_product(self, project_name, product_id): + def delete_product( + self, + project_name: str, + product_id: str, + ) -> DeleteOperation: """Delete product. Args: @@ -1216,19 +1255,19 @@ def delete_product(self, project_name, product_id): def create_version( self, - project_name, - version, - product_id, - task_id=None, - author=None, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - thumbnail_id=None, - version_id=None, - ): + project_name: str, + version: int, + product_id: str, + task_id: Optional[str] = None, + author: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + version_id: Optional[str] = None, + ) -> CreateOperation: """Create new version. Args: @@ -1276,18 +1315,18 @@ def create_version( def update_version( self, - project_name, - version_id, - version=None, - product_id=None, - task_id=NOT_SET, - attrib=None, - data=None, - tags=None, - status=None, - active=None, - thumbnail_id=NOT_SET, - ): + project_name: str, + version_id: str, + version: Optional[int] = None, + product_id: Optional[str] = None, + task_id: Optional[str] = NOT_SET, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + ) -> UpdateOperation: """Update version entity on server. Do not pass ``task_id`` amd ``thumbnail_id`` if you don't @@ -1341,7 +1380,11 @@ def update_version( project_name, "version", version_id, update_data ) - def delete_version(self, project_name, version_id): + def delete_version( + self, + project_name: str, + version_id: str, + ) -> DeleteOperation: """Delete version. Args: @@ -1358,18 +1401,18 @@ def delete_version(self, project_name, version_id): def create_representation( self, - project_name, - name, - version_id, - files=None, - attrib=None, - data=None, - traits=None, - tags=None, - status=None, - active=None, - representation_id=None, - ): + project_name: str, + name: str, + version_id: str, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + representation_id: Optional[str] = None, + ) -> CreateOperation: """Create new representation. Args: @@ -1379,7 +1422,7 @@ def create_representation( files (Optional[list[dict]]): Representation files information. attrib (Optional[dict[str, Any]]): Representation attributes. data (Optional[dict[str, Any]]): Representation data. - traits (Optional[Dict[str, Any]]): Representation traits. Empty + traits (Optional[dict[str, Any]]): Representation traits. Empty if not passed. tags (Optional[Iterable[str]]): Representation tags. status (Optional[str]): Representation status. @@ -1418,18 +1461,18 @@ def create_representation( def update_representation( self, - project_name, - representation_id, - name=None, - version_id=None, - files=None, - attrib=None, - data=None, - traits=None, - tags=None, - status=None, - active=None, - ): + project_name: str, + representation_id: str, + name: Optional[str] = None, + version_id: Optional[str] = None, + files: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + traits: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + ) -> UpdateOperation: """Update representation entity on server. Update of ``data`` will override existing value on folder entity. @@ -1446,7 +1489,7 @@ def update_representation( information. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. - traits (Optional[Dict[str, Any]]): New representation traits. + traits (Optional[dict[str, Any]]): New representation traits. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. @@ -1477,7 +1520,11 @@ def update_representation( update_data ) - def delete_representation(self, project_name, representation_id): + def delete_representation( + self, + project_name: str, + representation_id: str, + ) -> DeleteOperation: """Delete representation. Args: From 5eae3125befd9946553f7336843480fe79f88f1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 18 Aug 2025 16:29:46 +0200 Subject: [PATCH 162/506] fix missing variables --- ayon_api/operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 24c1f507d..e923143a0 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -6,7 +6,7 @@ import uuid from abc import ABC, abstractmethod import typing -from typing import Optional, Any +from typing import Optional, Any, Iterable from ._api import get_server_api_connection from .utils import create_entity_id, REMOVED_VALUE, NOT_SET @@ -644,7 +644,7 @@ class OperationsSession(object): is used if not passed. """ - def __init__(self, con: Optional["ServerApi"] = None) -> None: + def __init__(self, con: Optional["ServerAPI"] = None) -> None: if con is None: con = get_server_api_connection() self._con = con From 699d3748264448efbd359b932084028640b194c8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:48:13 +0200 Subject: [PATCH 163/506] don't use wrapped typehins --- automated_api.py | 4 +- ayon_api/_api.py | 287 ++++++++++--------- ayon_api/_api_helpers/actions.py | 24 +- ayon_api/_api_helpers/activities.py | 12 +- ayon_api/_api_helpers/attributes.py | 18 +- ayon_api/_api_helpers/base.py | 8 +- ayon_api/_api_helpers/bundles_addons.py | 19 +- ayon_api/_api_helpers/dependency_packages.py | 11 +- ayon_api/_api_helpers/events.py | 15 +- ayon_api/_api_helpers/folders.py | 20 +- ayon_api/_api_helpers/installers.py | 22 +- ayon_api/_api_helpers/links.py | 124 ++++---- ayon_api/_api_helpers/lists.py | 8 +- ayon_api/_api_helpers/products.py | 24 +- ayon_api/_api_helpers/projects.py | 22 +- ayon_api/_api_helpers/representations.py | 18 +- ayon_api/_api_helpers/secrets.py | 4 +- ayon_api/_api_helpers/tasks.py | 18 +- ayon_api/_api_helpers/thumbnails.py | 2 +- ayon_api/_api_helpers/versions.py | 24 +- ayon_api/_api_helpers/workfiles.py | 6 +- 21 files changed, 350 insertions(+), 340 deletions(-) diff --git a/automated_api.py b/automated_api.py index b32863cd0..db9348686 100644 --- a/automated_api.py +++ b/automated_api.py @@ -213,12 +213,12 @@ def _get_typehint(annotation, api_globals): _typehing_parents.append(parent) if _typehing_parents: - typehint = f'"{_typehint}"' + typehint = f'{_typehint}' for parent in reversed(_typehing_parents): typehint = f"{parent}[{typehint}]" return typehint - return f'"{typehint}"' + return f'{typehint}' def _get_param_typehint(param, api_globals): diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 0b44346ed..24a1bda94 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1252,7 +1252,7 @@ def send_batch_operations( def get_installers( version: Optional[str] = None, platform_name: Optional[str] = None, -) -> "InstallersInfoDict": +) -> InstallersInfoDict: """Information about desktop application installers on server. Desktop application installers are helpers to download/update AYON @@ -1284,7 +1284,7 @@ def create_installer( checksum_algorithm: str, file_size: int, sources: Optional[list[dict[str, Any]]] = None, -): +) -> None: """Create new installer information on server. This step will create only metadata. Make sure to upload installer @@ -1328,7 +1328,7 @@ def create_installer( def update_installer( filename: str, sources: list[dict[str, Any]], -): +) -> None: """Update installer information on server. Args: @@ -1346,7 +1346,7 @@ def update_installer( def delete_installer( filename: str, -): +) -> None: """Delete installer from server. Args: @@ -1364,7 +1364,7 @@ def download_installer( dst_filepath: str, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, -): +) -> TransferProgress: """Download installer file from server. Args: @@ -1374,6 +1374,9 @@ def download_installer( progress (Optional[TransferProgress]): Object that gives ability to track download progress. + Returns: + TransferProgress: Progress object. + """ con = get_server_api_connection() return con.download_installer( @@ -1388,7 +1391,7 @@ def upload_installer( src_filepath: str, dst_filename: str, progress: Optional[TransferProgress] = None, -): +) -> requests.Response: """Upload installer file to server. Args: @@ -1409,7 +1412,7 @@ def upload_installer( ) -def get_dependency_packages() -> "DependencyPackagesDict": +def get_dependency_packages() -> DependencyPackagesDict: """Information about dependency packages on server. To download dependency package, use 'download_dependency_package' @@ -1451,7 +1454,7 @@ def create_dependency_package( file_size: int, sources: Optional[list[dict[str, Any]]] = None, platform_name: Optional[str] = None, -): +) -> None: """Create dependency package on server. The package will be created on a server, it is also required to upload @@ -1498,7 +1501,7 @@ def create_dependency_package( def update_dependency_package( filename: str, sources: list[dict[str, Any]], -): +) -> None: """Update dependency package metadata on server. Args: @@ -1518,7 +1521,7 @@ def update_dependency_package( def delete_dependency_package( filename: str, platform_name: Optional[str] = None, -): +) -> None: """Remove dependency package for specific platform. Args: @@ -1577,7 +1580,7 @@ def upload_dependency_package( dst_filename: str, platform_name: Optional[str] = None, progress: Optional[TransferProgress] = None, -): +) -> None: """Upload dependency package to server. Args: @@ -1598,7 +1601,7 @@ def upload_dependency_package( ) -def get_secrets() -> list["SecretDict"]: +def get_secrets() -> list[SecretDict]: """Get all secrets. Example output:: @@ -1624,7 +1627,7 @@ def get_secrets() -> list["SecretDict"]: def get_secret( secret_name: str, -) -> "SecretDict": +) -> SecretDict: """Get secret by name. Example output:: @@ -1650,7 +1653,7 @@ def get_secret( def save_secret( secret_name: str, secret_value: str, -): +) -> None: """Save secret. This endpoint can create and update secret. @@ -1669,7 +1672,7 @@ def save_secret( def delete_secret( secret_name: str, -): +) -> None: """Delete secret by name. Args: @@ -1684,14 +1687,14 @@ def delete_secret( def get_actions( project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, -) -> list["ActionManifestDict"]: + mode: Optional[ActionModeType] = None, +) -> list[ActionManifestDict]: """Get actions for a context. Args: @@ -1728,13 +1731,13 @@ def trigger_action( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, -) -> "ActionTriggerResponse": +) -> ActionTriggerResponse: """Trigger action. Args: @@ -1772,13 +1775,13 @@ def get_action_config( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, -) -> "ActionConfigResponse": +) -> ActionConfigResponse: """Get action configuration. Args: @@ -1820,13 +1823,13 @@ def set_action_config( addon_version: str, value: dict[str, Any], project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, -) -> "ActionConfigResponse": +) -> ActionConfigResponse: """Set action configuration. Args: @@ -1867,7 +1870,7 @@ def set_action_config( def take_action( action_token: str, -) -> "ActionTakeResponse": +) -> ActionTakeResponse: """Take action metadata using an action token. Args: @@ -1905,13 +1908,13 @@ def abort_action( def get_activities( project_name: str, activity_ids: Optional[Iterable[str]] = None, - activity_types: Optional[Iterable["ActivityType"]] = 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["ActivityReferenceType"]] = None, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, fields: Optional[Iterable[str]] = None, limit: Optional[int] = None, order: Optional[SortOrder] = None, @@ -1962,7 +1965,7 @@ def get_activities( def get_activity_by_id( project_name: str, activity_id: str, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, fields: Optional[Iterable[str]] = None, ) -> Optional[dict[str, Any]]: """Get activity by id. @@ -1993,7 +1996,7 @@ def create_activity( project_name: str, entity_id: str, entity_type: str, - activity_type: "ActivityType", + activity_type: ActivityType, activity_id: Optional[str] = None, body: Optional[str] = None, file_ids: Optional[list[str]] = None, @@ -2039,7 +2042,7 @@ def update_activity( file_ids: Optional[list[str]] = None, append_file_ids: Optional[bool] = False, data: Optional[dict[str, Any]] = None, -): +) -> None: """Update activity by id. Args: @@ -2067,7 +2070,7 @@ def update_activity( def delete_activity( project_name: str, activity_id: str, -): +) -> None: """Delete activity by id. Args: @@ -2122,7 +2125,7 @@ def send_activities_batch_operations( ) -def get_bundles() -> "BundlesInfoDict": +def get_bundles() -> BundlesInfoDict: """Server bundles with basic information. This is example output:: @@ -2166,8 +2169,8 @@ def create_bundle( is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, -): + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, +) -> None: """Create bundle on server. Bundle cannot be changed once is created. Only isProduction, isStaging @@ -2229,8 +2232,8 @@ def update_bundle( is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, -): + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, +) -> None: """Update bundle on server. Dependency packages can be update only for single platform. Others @@ -2278,7 +2281,7 @@ def check_bundle_compatibility( is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[dict[str, "DevBundleAddonInfoDict"]] = None, + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, ) -> dict[str, Any]: """Check bundle compatibility. @@ -2320,7 +2323,7 @@ def check_bundle_compatibility( def delete_bundle( bundle_name: str, -): +) -> None: """Delete bundle from server. Args: @@ -2367,7 +2370,7 @@ def get_addon_endpoint( def get_addons_info( details: bool = True, -) -> "AddonsInfoDict": +) -> AddonsInfoDict: """Get information about addons available on server. Args: @@ -2940,7 +2943,7 @@ def get_events( topics: Optional[Iterable[str]] = None, event_ids: Optional[Iterable[str]] = None, project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[EventStatus]] = None, users: Optional[Iterable[str]] = None, include_logs: Optional[bool] = None, has_children: Optional[bool] = None, @@ -2961,7 +2964,7 @@ def get_events( event_ids (Optional[Iterable[str]]): Event ids. project_names (Optional[Iterable[str]]): Project on which event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. + statuses (Optional[Iterable[EventStatus]]): Filtering by statuses. users (Optional[Iterable[str]]): Filtering by users who created/triggered an event. include_logs (Optional[bool]): Query also log events. @@ -3007,13 +3010,13 @@ def update_event( sender: Optional[str] = None, project_name: Optional[str] = None, username: Optional[str] = None, - status: Optional[str] = None, + status: Optional[EventStatus] = None, description: Optional[str] = None, summary: Optional[dict[str, Any]] = None, payload: Optional[dict[str, Any]] = None, progress: Optional[int] = None, retries: Optional[int] = None, -): +) -> None: """Update event data. Args: @@ -3021,7 +3024,7 @@ def update_event( sender (Optional[str]): New sender of event. project_name (Optional[str]): New project name. username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", + status (Optional[EventStatus]): New event status. Enum: "pending", "in_progress", "finished", "failed", "aborted", "restarted" description (Optional[str]): New description. summary (Optional[dict[str, Any]]): New summary. @@ -3058,7 +3061,7 @@ def dispatch_event( finished: bool = True, store: bool = True, dependencies: Optional[list[str]] = None, -): +) -> RestApiResponse: """Dispatch event to server. Args: @@ -3073,8 +3076,8 @@ def dispatch_event( be used for simple filtering on listeners. payload (Optional[dict[str, Any]]): Full payload of event data with all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible future processing otherwise is event send only to active listeners. dependencies (Optional[list[str]]): Deprecated. @@ -3103,7 +3106,7 @@ def dispatch_event( def delete_event( event_id: str, -): +) -> None: """Delete event by id. Supported since AYON server 1.6.0. @@ -3122,16 +3125,16 @@ def delete_event( def enroll_event_job( - source_topic: "Union[str, list[str]]", + source_topic: Union[str, list[str]], target_topic: str, sender: str, description: Optional[str] = None, sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, + events_filter: Optional[EventFilter] = None, max_retries: Optional[int] = None, ignore_older_than: Optional[str] = None, ignore_sender_types: Optional[str] = None, -): +) -> Optional[EnrollEventData]: """Enroll job based on events. Enroll will find first unprocessed event with 'source_topic' and will @@ -3186,7 +3189,7 @@ def enroll_event_job( by given sender types. Returns: - Optional[dict[str, Any]]: None if there is no event matching + Optional[EnrollEventData]: None if there is no event matching filters. Created event with 'target_topic'. """ @@ -3206,25 +3209,25 @@ def enroll_event_job( def get_attributes_schema( use_cache: bool = True, -) -> "AttributesSchemaDict": +) -> AttributesSchemaDict: con = get_server_api_connection() return con.get_attributes_schema( use_cache=use_cache, ) -def reset_attributes_schema(): +def reset_attributes_schema() -> None: con = get_server_api_connection() return con.reset_attributes_schema() def set_attribute_config( attribute_name: str, - data: "AttributeSchemaDataDict", - scope: list["AttributeScope"], + data: AttributeSchemaDataDict, + scope: list[AttributeScope], position: Optional[int] = None, builtin: bool = False, -): +) -> None: con = get_server_api_connection() return con.set_attribute_config( attribute_name=attribute_name, @@ -3237,7 +3240,7 @@ def set_attribute_config( def remove_attribute_config( attribute_name: str, -): +) -> None: """Remove attribute from server. This can't be un-done, please use carefully. @@ -3253,8 +3256,8 @@ def remove_attribute_config( def get_attributes_for_type( - entity_type: "AttributeScope", -) -> dict[str, "AttributeSchemaDict"]: + entity_type: AttributeScope, +) -> dict[str, AttributeSchemaDict]: """Get attribute schemas available for an entity type. Example:: @@ -3298,7 +3301,7 @@ def get_attributes_for_type( def get_attributes_fields_for_type( - entity_type: "AttributeScope", + entity_type: AttributeScope, ) -> set[str]: """Prepare attribute fields for entity type. @@ -3312,7 +3315,7 @@ def get_attributes_fields_for_type( ) -def get_project_anatomy_presets() -> list["AnatomyPresetDict"]: +def get_project_anatomy_presets() -> list[AnatomyPresetDict]: """Anatomy presets available on server. Content has basic information about presets. Example output:: @@ -3354,7 +3357,7 @@ def get_default_anatomy_preset_name() -> str: def get_project_anatomy_preset( preset_name: Optional[str] = None, -) -> "AnatomyPresetDict": +) -> AnatomyPresetDict: """Anatomy preset values by name. Get anatomy preset values by preset name. Primary preset is returned @@ -3373,7 +3376,7 @@ def get_project_anatomy_preset( ) -def get_built_in_anatomy_preset() -> "AnatomyPresetDict": +def get_built_in_anatomy_preset() -> AnatomyPresetDict: """Get built-in anatomy preset. Returns: @@ -3384,14 +3387,14 @@ def get_built_in_anatomy_preset() -> "AnatomyPresetDict": return con.get_built_in_anatomy_preset() -def get_build_in_anatomy_preset() -> "AnatomyPresetDict": +def get_build_in_anatomy_preset() -> AnatomyPresetDict: con = get_server_api_connection() return con.get_build_in_anatomy_preset() def get_rest_project( project_name: str, -) -> Optional["ProjectDict"]: +) -> Optional[ProjectDict]: """Query project by name. This call returns project with anatomy data. @@ -3413,7 +3416,7 @@ def get_rest_project( def get_rest_projects( active: Optional[bool] = True, library: Optional[bool] = None, -) -> Generator["ProjectDict", None, None]: +) -> Generator[ProjectDict, None, None]: """Query available project entities. User must be logged in. @@ -3465,7 +3468,7 @@ def get_projects( library: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Generator["ProjectDict", None, None]: +) -> Generator[ProjectDict, None, None]: """Get projects. Args: @@ -3495,7 +3498,7 @@ def get_project( project_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["ProjectDict"]: +) -> Optional[ProjectDict]: """Get project. Args: @@ -3523,7 +3526,7 @@ def create_project( project_code: str, library_project: bool = False, preset_name: Optional[str] = None, -) -> "ProjectDict": +) -> ProjectDict: """Create project using AYON settings. This project creation function is not validating project entity on @@ -3573,7 +3576,7 @@ def update_project( active: Optional[bool] = None, project_code: Optional[str] = None, **changes, -): +) -> None: """Update project entity on server. Args: @@ -3796,7 +3799,7 @@ def get_project_roots_by_platform( def get_rest_folder( project_name: str, folder_id: str, -) -> Optional["FolderDict"]: +) -> Optional[FolderDict]: con = get_server_api_connection() return con.get_rest_folder( project_name=project_name, @@ -3807,7 +3810,7 @@ def get_rest_folder( def get_rest_folders( project_name: str, include_attrib: bool = False, -) -> list["FlatFolderDict"]: +) -> list[FlatFolderDict]: """Get simplified flat list of all project folders. Get all project folders in single REST call. This can be faster than @@ -3859,7 +3862,7 @@ def get_folders_hierarchy( project_name: str, search_string: Optional[str] = None, folder_types: Optional[Iterable[str]] = None, -) -> "ProjectHierarchyDict": +) -> ProjectHierarchyDict: """Get project hierarchy. All folders in project in hierarchy data structure. @@ -3903,7 +3906,7 @@ def get_folders_hierarchy( def get_folders_rest( project_name: str, include_attrib: bool = False, -) -> list["FlatFolderDict"]: +) -> list[FlatFolderDict]: """Get simplified flat list of all project folders. Get all project folders in single REST call. This can be faster than @@ -3975,7 +3978,7 @@ def get_folders( has_links: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Generator["FolderDict", None, None]: +) -> Generator[FolderDict, None, None]: """Query folders from server. Todos: @@ -4051,7 +4054,7 @@ def get_folder_by_id( folder_id: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["FolderDict"]: +) -> Optional[FolderDict]: """Query folder entity by id. Args: @@ -4082,7 +4085,7 @@ def get_folder_by_path( folder_path: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["FolderDict"]: +) -> Optional[FolderDict]: """Query folder entity by path. Folder path is a path to folder with all parent names joined by slash. @@ -4115,7 +4118,7 @@ def get_folder_by_name( folder_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["FolderDict"]: +) -> Optional[FolderDict]: """Query folder entity by path. Warnings: @@ -4238,7 +4241,7 @@ def update_folder( status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, -): +) -> None: """Update folder entity on server. Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't @@ -4286,7 +4289,7 @@ def delete_folder( project_name: str, folder_id: str, force: bool = False, -): +) -> None: """Delete folder. Args: @@ -4307,7 +4310,7 @@ def delete_folder( def get_rest_task( project_name: str, task_id: str, -) -> Optional["TaskDict"]: +) -> Optional[TaskDict]: con = get_server_api_connection() return con.get_rest_task( project_name=project_name, @@ -4328,7 +4331,7 @@ def get_tasks( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Generator["TaskDict", None, None]: +) -> Generator[TaskDict, None, None]: """Query task entities from server. Args: @@ -4383,7 +4386,7 @@ def get_task_by_name( task_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["TaskDict"]: +) -> Optional[TaskDict]: """Query task entity by name and folder id. Args: @@ -4415,7 +4418,7 @@ def get_task_by_id( task_id: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["TaskDict"]: +) -> Optional[TaskDict]: """Query task entity by id. Args: @@ -4452,7 +4455,7 @@ def get_tasks_by_folder_paths( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> dict[str, list["TaskDict"]]: +) -> dict[str, list[TaskDict]]: """Query task entities from server by folder paths. Args: @@ -4511,7 +4514,7 @@ def get_tasks_by_folder_path( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> list["TaskDict"]: +) -> list[TaskDict]: """Query task entities from server by folder path. Args: @@ -4560,7 +4563,7 @@ def get_task_by_folder_path( task_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, -) -> Optional["TaskDict"]: +) -> Optional[TaskDict]: """Query task entity by folder path and task name. Args: @@ -4655,7 +4658,7 @@ def update_task( status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, -): +) -> None: """Update task entity on server. Do not pass ``label`` amd ``thumbnail_id`` if you don't @@ -4704,7 +4707,7 @@ def update_task( def delete_task( project_name: str, task_id: str, -): +) -> None: """Delete task. Args: @@ -4722,7 +4725,7 @@ def delete_task( def get_rest_product( project_name: str, product_id: str, -) -> Optional["ProductDict"]: +) -> Optional[ProductDict]: con = get_server_api_connection() return con.get_rest_product( project_name=project_name, @@ -4744,7 +4747,7 @@ def get_products( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["ProductDict", None, None]: +) -> Generator[ProductDict, None, None]: """Query products from server. Todos: @@ -4804,7 +4807,7 @@ def get_product_by_id( product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: +) -> Optional[ProductDict]: """Query product entity by id. Args: @@ -4836,7 +4839,7 @@ def get_product_by_name( folder_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["ProductDict"]: +) -> Optional[ProductDict]: """Query product entity by name and folder id. Args: @@ -4866,7 +4869,7 @@ def get_product_by_name( def get_product_types( fields: Optional[Iterable[str]] = None, -) -> list["ProductTypeDict"]: +) -> list[ProductTypeDict]: """Types of products. This is server wide information. Product types have 'name', 'icon' and @@ -4888,7 +4891,7 @@ def get_product_types( def get_project_product_types( project_name: str, fields: Optional[Iterable[str]] = None, -) -> list["ProductTypeDict"]: +) -> list[ProductTypeDict]: """DEPRECATED Types of products available in a project. Filter only product types available in a project. @@ -4993,7 +4996,7 @@ def update_product( tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, -): +) -> None: """Update product entity on server. Update of ``data`` will override existing value on folder entity. @@ -5032,7 +5035,7 @@ def update_product( def delete_product( project_name: str, product_id: str, -): +) -> None: """Delete product. Args: @@ -5050,7 +5053,7 @@ def delete_product( def get_rest_version( project_name: str, version_id: str, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: con = get_server_api_connection() return con.get_rest_version( project_name=project_name, @@ -5072,7 +5075,7 @@ def get_versions( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: +) -> Generator[VersionDict, None, None]: """Get version entities based on passed filters from server. Args: @@ -5129,7 +5132,7 @@ def get_version_by_id( version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: """Query version entity by id. Args: @@ -5161,7 +5164,7 @@ def get_version_by_name( product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: """Query version entity by version and product id. Args: @@ -5194,7 +5197,7 @@ def get_hero_version_by_id( version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: """Query hero version entity by id. Args: @@ -5225,7 +5228,7 @@ def get_hero_version_by_product_id( product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: """Query hero version entity by product id. Only one hero version is available on a product. @@ -5260,7 +5263,7 @@ def get_hero_versions( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["VersionDict", None, None]: +) -> Generator[VersionDict, None, None]: """Query hero versions by multiple filters. Only one hero version is available on a product. @@ -5299,7 +5302,7 @@ def get_last_versions( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> dict[str, Optional["VersionDict"]]: +) -> dict[str, Optional[VersionDict]]: """Query last version entities by product ids. Args: @@ -5332,7 +5335,7 @@ def get_last_version_by_product_id( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: """Query last version entity by product id. Args: @@ -5366,7 +5369,7 @@ def get_last_version_by_product_name( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["VersionDict"]: +) -> Optional[VersionDict]: """Query last version entity by product name and folder id. Args: @@ -5481,7 +5484,7 @@ def update_version( status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, -): +) -> None: """Update version entity on server. Do not pass ``task_id`` amd ``thumbnail_id`` if you don't @@ -5528,7 +5531,7 @@ def update_version( def delete_version( project_name: str, version_id: str, -): +) -> None: """Delete version. Args: @@ -5546,7 +5549,7 @@ def delete_version( def get_rest_representation( project_name: str, representation_id: str, -) -> Optional["RepresentationDict"]: +) -> Optional[RepresentationDict]: con = get_server_api_connection() return con.get_rest_representation( project_name=project_name, @@ -5566,7 +5569,7 @@ def get_representations( has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["RepresentationDict", None, None]: +) -> Generator[RepresentationDict, None, None]: """Get representation entities based on passed filters from server. .. todo:: @@ -5626,7 +5629,7 @@ def get_representation_by_id( representation_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: +) -> Optional[RepresentationDict]: """Query representation entity from server based on id filter. Args: @@ -5657,7 +5660,7 @@ def get_representation_by_name( version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["RepresentationDict"]: +) -> Optional[RepresentationDict]: """Query representation entity by name and version id. Args: @@ -5816,7 +5819,7 @@ def get_representation_parents( folder_fields: Optional[Iterable[str]] = None, product_fields: Optional[Iterable[str]] = None, version_fields: Optional[Iterable[str]] = None, -) -> Optional["RepresentationParents"]: +) -> Optional[RepresentationParents]: """Find representation parents by representation id. Representation parent entities up to project. @@ -5962,7 +5965,7 @@ def update_representation( tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, -): +) -> None: """Update representation entity on server. Update of ``data`` will override existing value on folder entity. @@ -6004,7 +6007,7 @@ def update_representation( def delete_representation( project_name: str, representation_id: str, -): +) -> None: """Delete representation. Args: @@ -6030,7 +6033,7 @@ def get_workfiles_info( has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Generator["WorkfileInfoDict", None, None]: +) -> Generator[WorkfileInfoDict, None, None]: """Workfile info entities by passed filters. Args: @@ -6077,7 +6080,7 @@ def get_workfile_info( path: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: +) -> Optional[WorkfileInfoDict]: """Workfile info entity by task id and workfile path. Args: @@ -6109,7 +6112,7 @@ def get_workfile_info_by_id( workfile_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, -) -> Optional["WorkfileInfoDict"]: +) -> Optional[WorkfileInfoDict]: """Workfile info entity by id. Args: @@ -6232,7 +6235,7 @@ def create_link_type( input_type: str, output_type: str, data: Optional[dict[str, Any]] = None, -): +) -> None: """Create or update link type on server. Warning: @@ -6264,7 +6267,7 @@ def delete_link_type( link_type_name: str, input_type: str, output_type: str, -): +) -> None: """Remove link type from project. Args: @@ -6292,7 +6295,7 @@ def make_sure_link_type_exists( input_type: str, output_type: str, data: Optional[dict[str, Any]] = None, -): +) -> None: """Make sure link type exists on a project. Args: @@ -6321,7 +6324,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, -): +) -> CreateLinkData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -6343,7 +6346,7 @@ def create_link( Available from server version '1.0.0-rc.6'. Returns: - dict[str, str]: Information about link. + CreateLinkData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -6364,7 +6367,7 @@ def create_link( def delete_link( project_name: str, link_id: str, -): +) -> None: """Remove link by id. Args: @@ -6387,7 +6390,7 @@ def get_entities_links( entity_type: str, entity_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, link_names: Optional[Iterable[str]] = None, link_name_regex: Optional[str] = None, ) -> dict[str, list[dict[str, Any]]]: @@ -6446,7 +6449,7 @@ def get_folders_links( project_name: str, folder_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query folders links from server. @@ -6475,7 +6478,7 @@ def get_folder_links( project_name: str, folder_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query folder links from server. @@ -6503,7 +6506,7 @@ def get_tasks_links( project_name: str, task_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query tasks links from server. @@ -6532,7 +6535,7 @@ def get_task_links( project_name: str, task_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query task links from server. @@ -6560,7 +6563,7 @@ def get_products_links( project_name: str, product_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query products links from server. @@ -6589,7 +6592,7 @@ def get_product_links( project_name: str, product_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query product links from server. @@ -6617,7 +6620,7 @@ def get_versions_links( project_name: str, version_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query versions links from server. @@ -6646,7 +6649,7 @@ def get_version_links( project_name: str, version_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query version links from server. @@ -6674,7 +6677,7 @@ def get_representations_links( project_name: str, representation_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query representations links from server. @@ -6703,7 +6706,7 @@ def get_representation_links( project_name: str, representation_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query representation links from server. @@ -6805,7 +6808,7 @@ def get_entity_list_by_id( def create_entity_list( project_name: str, - entity_type: "EntityListEntityType", + entity_type: EntityListEntityType, label: str, *, list_type: Optional[str] = None, @@ -6920,7 +6923,7 @@ def delete_entity_list( def get_entity_list_attribute_definitions( project_name: str, list_id: str, -) -> list["EntityListAttributeDefinitionDict"]: +) -> list[EntityListAttributeDefinitionDict]: """Get attribute definitioins on entity list. Args: @@ -6942,7 +6945,7 @@ def get_entity_list_attribute_definitions( def set_entity_list_attribute_definitions( project_name: str, list_id: str, - attribute_definitions: list["EntityListAttributeDefinitionDict"], + attribute_definitions: list[EntityListAttributeDefinitionDict], ) -> None: """Set attribute definitioins on entity list. @@ -7005,7 +7008,7 @@ def update_entity_list_items( project_name: str, list_id: str, items: list[dict[str, Any]], - mode: "EntityListItemMode", + mode: EntityListItemMode, ) -> None: """Update items in entity list. @@ -7300,7 +7303,7 @@ def update_thumbnail( project_name: str, thumbnail_id: str, src_filepath: str, -): +) -> None: """Change thumbnail content by id. Update can be also used to create new thumbnail. diff --git a/ayon_api/_api_helpers/actions.py b/ayon_api/_api_helpers/actions.py index 136bf29cb..37ec2f519 100644 --- a/ayon_api/_api_helpers/actions.py +++ b/ayon_api/_api_helpers/actions.py @@ -23,14 +23,14 @@ class ActionsAPI(BaseServerAPI): def get_actions( self, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - mode: Optional["ActionModeType"] = None, - ) -> list["ActionManifestDict"]: + mode: Optional[ActionModeType] = None, + ) -> list[ActionManifestDict]: """Get actions for a context. Args: @@ -77,13 +77,13 @@ def trigger_action( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - ) -> "ActionTriggerResponse": + ) -> ActionTriggerResponse: """Trigger action. Args: @@ -134,13 +134,13 @@ def get_action_config( addon_name: str, addon_version: str, project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - ) -> "ActionConfigResponse": + ) -> ActionConfigResponse: """Get action configuration. Args: @@ -182,13 +182,13 @@ def set_action_config( addon_version: str, value: dict[str, Any], project_name: Optional[str] = None, - entity_type: Optional["ActionEntityTypes"] = None, + entity_type: Optional[ActionEntityTypes] = None, entity_ids: Optional[list[str]] = None, entity_subtypes: Optional[list[str]] = None, form_data: Optional[dict[str, Any]] = None, *, variant: Optional[str] = None, - ) -> "ActionConfigResponse": + ) -> ActionConfigResponse: """Set action configuration. Args: @@ -225,7 +225,7 @@ def set_action_config( variant, ) - def take_action(self, action_token: str) -> "ActionTakeResponse": + def take_action(self, action_token: str) -> ActionTakeResponse: """Take action metadata using an action token. Args: @@ -267,12 +267,12 @@ def _send_config_request( addon_version: str, value: Optional[dict[str, Any]], project_name: Optional[str], - entity_type: Optional["ActionEntityTypes"], + entity_type: Optional[ActionEntityTypes], entity_ids: Optional[list[str]], entity_subtypes: Optional[list[str]], form_data: Optional[dict[str, Any]], variant: Optional[str], - ) -> "ActionConfigResponse": + ) -> ActionConfigResponse: """Set and get action configuration.""" if variant is None: variant = self.get_default_settings_variant() diff --git a/ayon_api/_api_helpers/activities.py b/ayon_api/_api_helpers/activities.py index c0c58406d..f7ae3d4f7 100644 --- a/ayon_api/_api_helpers/activities.py +++ b/ayon_api/_api_helpers/activities.py @@ -24,13 +24,13 @@ def get_activities( self, project_name: str, activity_ids: Optional[Iterable[str]] = None, - activity_types: Optional[Iterable["ActivityType"]] = 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["ActivityReferenceType"]] = None, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, fields: Optional[Iterable[str]] = None, limit: Optional[int] = None, order: Optional[SortOrder] = None, @@ -109,7 +109,7 @@ def get_activity_by_id( self, project_name: str, activity_id: str, - reference_types: Optional[Iterable["ActivityReferenceType"]] = None, + reference_types: Optional[Iterable[ActivityReferenceType]] = None, fields: Optional[Iterable[str]] = None, ) -> Optional[dict[str, Any]]: """Get activity by id. @@ -141,7 +141,7 @@ def create_activity( project_name: str, entity_id: str, entity_type: str, - activity_type: "ActivityType", + activity_type: ActivityType, activity_id: Optional[str] = None, body: Optional[str] = None, file_ids: Optional[list[str]] = None, @@ -194,7 +194,7 @@ def update_activity( file_ids: Optional[list[str]] = None, append_file_ids: Optional[bool] = False, data: Optional[dict[str, Any]] = None, - ): + ) -> None: """Update activity by id. Args: @@ -242,7 +242,7 @@ def update_activity( ) response.raise_for_status() - def delete_activity(self, project_name: str, activity_id: str): + def delete_activity(self, project_name: str, activity_id: str) -> None: """Delete activity by id. Args: diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py index 9a9216d0f..26db47484 100644 --- a/ayon_api/_api_helpers/attributes.py +++ b/ayon_api/_api_helpers/attributes.py @@ -21,7 +21,7 @@ class AttributesAPI(BaseServerAPI): def get_attributes_schema( self, use_cache: bool = True - ) -> "AttributesSchemaDict": + ) -> AttributesSchemaDict: if not use_cache: self.reset_attributes_schema() @@ -31,18 +31,18 @@ def get_attributes_schema( self._attributes_schema = result.data return copy.deepcopy(self._attributes_schema) - def reset_attributes_schema(self): + def reset_attributes_schema(self) -> None: self._attributes_schema = None self._entity_type_attributes_cache = {} def set_attribute_config( self, attribute_name: str, - data: "AttributeSchemaDataDict", - scope: list["AttributeScope"], + data: AttributeSchemaDataDict, + scope: list[AttributeScope], position: Optional[int] = None, builtin: bool = False, - ): + ) -> None: if position is None: attributes = self.get("attributes").data["attributes"] origin_attr = next( @@ -73,7 +73,7 @@ def set_attribute_config( self.reset_attributes_schema() - def remove_attribute_config(self, attribute_name: str): + def remove_attribute_config(self, attribute_name: str) -> None: """Remove attribute from server. This can't be un-done, please use carefully. @@ -91,8 +91,8 @@ def remove_attribute_config(self, attribute_name: str): self.reset_attributes_schema() def get_attributes_for_type( - self, entity_type: "AttributeScope" - ) -> dict[str, "AttributeSchemaDict"]: + self, entity_type: AttributeScope + ) -> dict[str, AttributeSchemaDict]: """Get attribute schemas available for an entity type. Example:: @@ -144,7 +144,7 @@ def get_attributes_for_type( return copy.deepcopy(attributes) def get_attributes_fields_for_type( - self, entity_type: "AttributeScope" + self, entity_type: AttributeScope ) -> set[str]: """Prepare attribute fields for entity type. diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index f785080ab..7c792d42d 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -21,7 +21,7 @@ class BaseServerAPI: def get_server_version(self) -> str: raise NotImplementedError() - def get_server_version_tuple(self) -> "ServerVersion": + def get_server_version_tuple(self) -> ServerVersion: raise NotImplementedError() def get_base_url(self) -> str: @@ -93,7 +93,7 @@ def get_rest_entity_by_id( project_name: str, entity_type: str, entity_id: str, - ) -> Optional["AnyEntityDict"]: + ) -> Optional[AnyEntityDict]: raise NotImplementedError() def get_project( @@ -101,7 +101,7 @@ def get_project( project_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Optional["ProjectDict"]: + ) -> Optional[ProjectDict]: raise NotImplementedError() def _prepare_fields( @@ -112,7 +112,7 @@ def _prepare_fields( ): raise NotImplementedError() - def _convert_entity_data(self, entity: "AnyEntityDict"): + def _convert_entity_data(self, entity: AnyEntityDict): raise NotImplementedError() def _send_batch_operations( diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index dfdd3b591..1b70967ec 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -21,7 +21,7 @@ class BundlesAddonsAPI(BaseServerAPI): - def get_bundles(self) -> "BundlesInfoDict": + def get_bundles(self) -> BundlesInfoDict: """Server bundles with basic information. This is example output:: @@ -66,9 +66,8 @@ def create_bundle( is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - dict[str, "DevBundleAddonInfoDict"]] = None, - ): + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, + ) -> None: """Create bundle on server. Bundle cannot be changed once is created. Only isProduction, isStaging @@ -137,9 +136,8 @@ def update_bundle( is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - dict[str, "DevBundleAddonInfoDict"]] = None, - ): + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, + ) -> None: """Update bundle on server. Dependency packages can be update only for single platform. Others @@ -195,8 +193,7 @@ def check_bundle_compatibility( is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, dev_active_user: Optional[str] = None, - dev_addons_config: Optional[ - dict[str, "DevBundleAddonInfoDict"]] = None, + dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, ) -> dict[str, Any]: """Check bundle compatibility. @@ -243,7 +240,7 @@ def check_bundle_compatibility( response.raise_for_status() return response.data - def delete_bundle(self, bundle_name: str): + def delete_bundle(self, bundle_name: str) -> None: """Delete bundle from server. Args: @@ -283,7 +280,7 @@ def get_addon_endpoint( ending = f"/{'/'.join(subpaths)}" return f"addons/{addon_name}/{addon_version}{ending}" - def get_addons_info(self, details: bool = True) -> "AddonsInfoDict": + def get_addons_info(self, details: bool = True) -> AddonsInfoDict: """Get information about addons available on server. Args: diff --git a/ayon_api/_api_helpers/dependency_packages.py b/ayon_api/_api_helpers/dependency_packages.py index 7268d8a77..dc1f43e94 100644 --- a/ayon_api/_api_helpers/dependency_packages.py +++ b/ayon_api/_api_helpers/dependency_packages.py @@ -15,7 +15,7 @@ class DependencyPackagesAPI(BaseServerAPI): - def get_dependency_packages(self) -> "DependencyPackagesDict": + def get_dependency_packages(self) -> DependencyPackagesDict: """Information about dependency packages on server. To download dependency package, use 'download_dependency_package' @@ -59,7 +59,7 @@ def create_dependency_package( file_size: int, sources: Optional[list[dict[str, Any]]] = None, platform_name: Optional[str] = None, - ): + ) -> None: """Create dependency package on server. The package will be created on a server, it is also required to upload @@ -108,7 +108,7 @@ def create_dependency_package( def update_dependency_package( self, filename: str, sources: list[dict[str, Any]] - ): + ) -> None: """Update dependency package metadata on server. Args: @@ -126,7 +126,7 @@ def update_dependency_package( def delete_dependency_package( self, filename: str, platform_name: Optional[str] = None - ): + ) -> None: """Remove dependency package for specific platform. Args: @@ -147,7 +147,6 @@ def delete_dependency_package( route = self._get_dependency_package_route(filename) response = self.delete(route) response.raise_for_status("Failed to delete dependency file") - return response.data def download_dependency_package( self, @@ -203,7 +202,7 @@ def upload_dependency_package( dst_filename: str, platform_name: Optional[str] = None, progress: Optional[TransferProgress] = None, - ): + ) -> None: """Upload dependency package to server. Args: diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index f028d5c9e..8f2267f77 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -145,7 +145,7 @@ def update_event( payload: Optional[dict[str, Any]] = None, progress: Optional[int] = None, retries: Optional[int] = None, - ): + ) -> None: """Update event data. Args: @@ -198,7 +198,7 @@ def dispatch_event( finished: bool = True, store: bool = True, dependencies: Optional[list[str]] = None, - ): + ) -> RestApiResponse: """Dispatch event to server. Args: @@ -213,8 +213,8 @@ def dispatch_event( be used for simple filtering on listeners. payload (Optional[dict[str, Any]]): Full payload of event data with all details. - finished (Optional[bool]): Mark event as finished on dispatch. - store (Optional[bool]): Store event in event queue for possible + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible future processing otherwise is event send only to active listeners. dependencies (Optional[list[str]]): Deprecated. @@ -256,7 +256,7 @@ def dispatch_event( response.raise_for_status() return response - def delete_event(self, event_id: str): + def delete_event(self, event_id: str) -> None: """Delete event by id. Supported since AYON server 1.6.0. @@ -270,16 +270,15 @@ def delete_event(self, event_id: str): """ response = self.delete(f"events/{event_id}") response.raise_for_status() - return response def enroll_event_job( self, - source_topic: "Union[str, list[str]]", + source_topic: Union[str, list[str]], target_topic: str, sender: str, description: Optional[str] = None, sequential: Optional[bool] = None, - events_filter: Optional["EventFilter"] = None, + events_filter: Optional[EventFilter] = None, max_retries: Optional[int] = None, ignore_older_than: Optional[str] = None, ignore_sender_types: Optional[str] = None, diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index a0805b73f..fbef4e485 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -27,14 +27,14 @@ class FoldersAPI(BaseServerAPI): def get_rest_folder( self, project_name: str, folder_id: str - ) -> Optional["FolderDict"]: + ) -> Optional[FolderDict]: return self.get_rest_entity_by_id( project_name, "folder", folder_id ) def get_rest_folders( self, project_name: str, include_attrib: bool = False - ) -> list["FlatFolderDict"]: + ) -> list[FlatFolderDict]: """Get simplified flat list of all project folders. Get all project folders in single REST call. This can be faster than @@ -95,7 +95,7 @@ def get_folders_hierarchy( project_name: str, search_string: Optional[str] = None, folder_types: Optional[Iterable[str]] = None - ) -> "ProjectHierarchyDict": + ) -> ProjectHierarchyDict: """Get project hierarchy. All folders in project in hierarchy data structure. @@ -143,7 +143,7 @@ def get_folders_hierarchy( def get_folders_rest( self, project_name: str, include_attrib: bool = False - ) -> list["FlatFolderDict"]: + ) -> list[FlatFolderDict]: """Get simplified flat list of all project folders. Get all project folders in single REST call. This can be faster than @@ -218,7 +218,7 @@ def get_folders( has_links: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False - ) -> Generator["FolderDict", None, None]: + ) -> Generator[FolderDict, None, None]: """Query folders from server. Todos: @@ -348,7 +348,7 @@ def get_folder_by_id( folder_id: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Optional["FolderDict"]: + ) -> Optional[FolderDict]: """Query folder entity by id. Args: @@ -382,7 +382,7 @@ def get_folder_by_path( folder_path: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Optional["FolderDict"]: + ) -> Optional[FolderDict]: """Query folder entity by path. Folder path is a path to folder with all parent names joined by slash. @@ -418,7 +418,7 @@ def get_folder_by_name( folder_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Optional["FolderDict"]: + ) -> Optional[FolderDict]: """Query folder entity by path. Warnings: @@ -565,7 +565,7 @@ def update_folder( status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, - ): + ) -> None: """Update folder entity on server. Do not pass ``parent_id``, ``label`` amd ``thumbnail_id`` if you don't @@ -621,7 +621,7 @@ def update_folder( def delete_folder( self, project_name: str, folder_id: str, force: bool = False - ): + ) -> None: """Delete folder. Args: diff --git a/ayon_api/_api_helpers/installers.py b/ayon_api/_api_helpers/installers.py index f6d7ec7f5..be2bcaaec 100644 --- a/ayon_api/_api_helpers/installers.py +++ b/ayon_api/_api_helpers/installers.py @@ -1,9 +1,10 @@ from __future__ import annotations - import typing from typing import Optional, Any +import requests + from ayon_api.utils import prepare_query_string, TransferProgress from .base import BaseServerAPI @@ -17,7 +18,7 @@ def get_installers( self, version: Optional[str] = None, platform_name: Optional[str] = None, - ) -> "InstallersInfoDict": + ) -> InstallersInfoDict: """Information about desktop application installers on server. Desktop application installers are helpers to download/update AYON @@ -51,7 +52,7 @@ def create_installer( checksum_algorithm: str, file_size: int, sources: Optional[list[dict[str, Any]]] = None, - ): + ) -> None: """Create new installer information on server. This step will create only metadata. Make sure to upload installer @@ -94,7 +95,9 @@ def create_installer( response = self.post("desktop/installers", **body) response.raise_for_status() - def update_installer(self, filename: str, sources: list[dict[str, Any]]): + def update_installer( + self, filename: str, sources: list[dict[str, Any]] + ) -> None: """Update installer information on server. Args: @@ -109,7 +112,7 @@ def update_installer(self, filename: str, sources: list[dict[str, Any]]): ) response.raise_for_status() - def delete_installer(self, filename: str): + def delete_installer(self, filename: str) -> None: """Delete installer from server. Args: @@ -125,7 +128,7 @@ def download_installer( dst_filepath: str, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None - ): + ) -> TransferProgress: """Download installer file from server. Args: @@ -135,8 +138,11 @@ def download_installer( progress (Optional[TransferProgress]): Object that gives ability to track download progress. + Returns: + TransferProgress: Progress object. + """ - self.download_file( + return self.download_file( f"desktop/installers/{filename}", dst_filepath, chunk_size=chunk_size, @@ -148,7 +154,7 @@ def upload_installer( src_filepath: str, dst_filename: str, progress: Optional[TransferProgress] = None, - ): + ) -> requests.Response: """Upload installer file to server. Args: diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index f0a5f6580..6c23b504c 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -15,8 +15,12 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: + from typing import TypedDict from ayon_api.typing import LinkDirection + class CreateLinkData(TypedDict): + id: str + class LinksAPI(BaseServerAPI): def get_full_link_type_name( @@ -106,7 +110,7 @@ def create_link_type( input_type: str, output_type: str, data: Optional[dict[str, Any]] = None, - ): + ) -> None: """Create or update link type on server. Warning: @@ -140,7 +144,7 @@ def delete_link_type( link_type_name: str, input_type: str, output_type: str, - ): + ) -> None: """Remove link type from project. Args: @@ -168,7 +172,7 @@ def make_sure_link_type_exists( input_type: str, output_type: str, data: Optional[dict[str, Any]] = None, - ): + ) -> None: """Make sure link type exists on a project. Args: @@ -199,7 +203,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, - ): + ) -> CreateLinkData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -221,7 +225,7 @@ def create_link( Available from server version '1.0.0-rc.6'. Returns: - dict[str, str]: Information about link. + CreateLinkData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -244,7 +248,7 @@ def create_link( response.raise_for_status() return response.data - def delete_link(self, project_name: str, link_id: str): + def delete_link(self, project_name: str, link_id: str) -> None: """Remove link by id. Args: @@ -260,56 +264,13 @@ def delete_link(self, project_name: str, link_id: str): ) response.raise_for_status() - def _prepare_link_filters( - self, - filters: dict[str, Any], - link_types: Optional[Iterable[str], None], - link_direction: Optional["LinkDirection"], - link_names: Optional[Iterable[str]], - link_name_regex: Optional[str], - ) -> bool: - """Add links filters for GraphQl queries. - - Args: - filters (dict[str, Any]): Object where filters will be added. - link_types (Optional[Iterable[str]]): Link types filters. - link_direction (Optional[Literal["in", "out"]]): Direction of - link "in", "out" or 'None' for both. - link_names (Optional[Iterable[str]]): Link name filters. - link_name_regex (Optional[str]): Regex filter for link name. - - Returns: - bool: Links are valid, and query from server can happen. - - """ - if link_types is not None: - link_types = set(link_types) - if not link_types: - return False - filters["linkTypes"] = list(link_types) - - if link_names is not None: - link_names = set(link_names) - if not link_names: - return False - filters["linkNames"] = list(link_names) - - if link_direction is not None: - if link_direction not in ("in", "out"): - return False - filters["linkDirection"] = link_direction - - if link_name_regex is not None: - filters["linkNameRegex"] = link_name_regex - return True - def get_entities_links( self, project_name: str, entity_type: str, entity_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, link_names: Optional[Iterable[str]] = None, link_name_regex: Optional[str] = None, ) -> dict[str, list[dict[str, Any]]]: @@ -411,7 +372,7 @@ def get_folders_links( project_name: str, folder_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query folders links from server. @@ -436,7 +397,7 @@ def get_folder_links( project_name: str, folder_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query folder links from server. @@ -460,7 +421,7 @@ def get_tasks_links( project_name: str, task_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query tasks links from server. @@ -485,7 +446,7 @@ def get_task_links( project_name: str, task_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query task links from server. @@ -509,7 +470,7 @@ def get_products_links( project_name: str, product_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query products links from server. @@ -534,7 +495,7 @@ def get_product_links( project_name: str, product_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query product links from server. @@ -558,7 +519,7 @@ def get_versions_links( project_name: str, version_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query versions links from server. @@ -583,7 +544,7 @@ def get_version_links( project_name: str, version_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> list[dict[str, Any]]: """Query version links from server. @@ -607,7 +568,7 @@ def get_representations_links( project_name: str, representation_ids: Optional[Iterable[str]] = None, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None, + link_direction: Optional[LinkDirection] = None, ) -> dict[str, list[dict[str, Any]]]: """Query representations links from server. @@ -636,7 +597,7 @@ def get_representation_links( project_name: str, representation_id: str, link_types: Optional[Iterable[str]] = None, - link_direction: Optional["LinkDirection"] = None + link_direction: Optional[LinkDirection] = None ) -> list[dict[str, Any]]: """Query representation links from server. @@ -655,3 +616,46 @@ def get_representation_links( return self.get_representations_links( project_name, [representation_id], link_types, link_direction )[representation_id] + + def _prepare_link_filters( + self, + filters: dict[str, Any], + link_types: Optional[Iterable[str], None], + link_direction: Optional[LinkDirection], + link_names: Optional[Iterable[str]], + link_name_regex: Optional[str], + ) -> bool: + """Add links filters for GraphQl queries. + + Args: + filters (dict[str, Any]): Object where filters will be added. + link_types (Optional[Iterable[str]]): Link types filters. + link_direction (Optional[Literal["in", "out"]]): Direction of + link "in", "out" or 'None' for both. + link_names (Optional[Iterable[str]]): Link name filters. + link_name_regex (Optional[str]): Regex filter for link name. + + Returns: + bool: Links are valid, and query from server can happen. + + """ + if link_types is not None: + link_types = set(link_types) + if not link_types: + return False + filters["linkTypes"] = list(link_types) + + if link_names is not None: + link_names = set(link_names) + if not link_names: + return False + filters["linkNames"] = list(link_names) + + if link_direction is not None: + if link_direction not in ("in", "out"): + return False + filters["linkDirection"] = link_direction + + if link_name_regex is not None: + filters["linkNameRegex"] = link_name_regex + return True diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index f796723fd..b6bd79265 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -113,7 +113,7 @@ def get_entity_list_by_id( def create_entity_list( self, project_name: str, - entity_type: "EntityListEntityType", + entity_type: EntityListEntityType, label: str, *, list_type: Optional[str] = None, @@ -237,7 +237,7 @@ def delete_entity_list(self, project_name: str, list_id: str) -> None: def get_entity_list_attribute_definitions( self, project_name: str, list_id: str - ) -> list["EntityListAttributeDefinitionDict"]: + ) -> list[EntityListAttributeDefinitionDict]: """Get attribute definitioins on entity list. Args: @@ -259,7 +259,7 @@ def set_entity_list_attribute_definitions( self, project_name: str, list_id: str, - attribute_definitions: list["EntityListAttributeDefinitionDict"], + attribute_definitions: list[EntityListAttributeDefinitionDict], ) -> None: """Set attribute definitioins on entity list. @@ -332,7 +332,7 @@ def update_entity_list_items( project_name: str, list_id: str, items: list[dict[str, Any]], - mode: "EntityListItemMode", + mode: EntityListItemMode, ) -> None: """Update items in entity list. diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index 5ce78f97c..13fdb9b80 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -23,8 +23,10 @@ class ProductsAPI(BaseServerAPI): def get_rest_product( self, project_name: str, product_id: str - ) -> Optional["ProductDict"]: - return self.get_rest_entity_by_id(project_name, "product", product_id) + ) -> Optional[ProductDict]: + return self.get_rest_entity_by_id( + project_name, "product", product_id + ) def get_products( self, @@ -41,7 +43,7 @@ def get_products( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Generator["ProductDict", None, None]: + ) -> Generator[ProductDict, None, None]: """Query products from server. Todos: @@ -197,7 +199,7 @@ def get_product_by_id( product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Optional["ProductDict"]: + ) -> Optional[ProductDict]: """Query product entity by id. Args: @@ -232,7 +234,7 @@ def get_product_by_name( folder_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Optional["ProductDict"]: + ) -> Optional[ProductDict]: """Query product entity by name and folder id. Args: @@ -264,7 +266,7 @@ def get_product_by_name( def get_product_types( self, fields: Optional[Iterable[str]] = None - ) -> list["ProductTypeDict"]: + ) -> list[ProductTypeDict]: """Types of products. This is server wide information. Product types have 'name', 'icon' and @@ -288,7 +290,7 @@ def get_product_types( def get_project_product_types( self, project_name: str, fields: Optional[Iterable[str]] = None - ) -> list["ProductTypeDict"]: + ) -> list[ProductTypeDict]: """DEPRECATED Types of products available in a project. Filter only product types available in a project. @@ -434,7 +436,7 @@ def update_product( tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - ): + ) -> None: """Update product entity on server. Update of ``data`` will override existing value on folder entity. @@ -475,7 +477,7 @@ def update_product( ) response.raise_for_status() - def delete_product(self, project_name: str, product_id: str): + def delete_product(self, project_name: str, product_id: str) -> None: """Delete product. Args: @@ -491,9 +493,9 @@ def delete_product(self, project_name: str, product_id: str): def _filter_product( self, project_name: str, - product: "ProductDict", + product: ProductDict, active: Optional[bool], - ) -> Optional["ProductDict"]: + ) -> Optional[ProductDict]: if active is not None and product["active"] is not active: return None diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index c54340458..09fe66d98 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -17,7 +17,7 @@ class ProjectsAPI(BaseServerAPI): - def get_project_anatomy_presets(self) -> list["AnatomyPresetDict"]: + def get_project_anatomy_presets(self) -> list[AnatomyPresetDict]: """Anatomy presets available on server. Content has basic information about presets. Example output:: @@ -60,7 +60,7 @@ def get_default_anatomy_preset_name(self) -> str: def get_project_anatomy_preset( self, preset_name: Optional[str] = None - ) -> "AnatomyPresetDict": + ) -> AnatomyPresetDict: """Anatomy preset values by name. Get anatomy preset values by preset name. Primary preset is returned @@ -83,7 +83,7 @@ def get_project_anatomy_preset( result.raise_for_status() return result.data - def get_built_in_anatomy_preset(self) -> "AnatomyPresetDict": + def get_built_in_anatomy_preset(self) -> AnatomyPresetDict: """Get built-in anatomy preset. Returns: @@ -96,7 +96,7 @@ def get_built_in_anatomy_preset(self) -> "AnatomyPresetDict": preset_name = "_" return self.get_project_anatomy_preset(preset_name) - def get_build_in_anatomy_preset(self) -> "AnatomyPresetDict": + def get_build_in_anatomy_preset(self) -> AnatomyPresetDict: warnings.warn( ( "Used deprecated 'get_build_in_anatomy_preset' use" @@ -108,7 +108,7 @@ def get_build_in_anatomy_preset(self) -> "AnatomyPresetDict": def get_rest_project( self, project_name: str - ) -> Optional["ProjectDict"]: + ) -> Optional[ProjectDict]: """Query project by name. This call returns project with anatomy data. @@ -136,7 +136,7 @@ def get_rest_projects( self, active: Optional[bool] = True, library: Optional[bool] = None, - ) -> Generator["ProjectDict", None, None]: + ) -> Generator[ProjectDict, None, None]: """Query available project entities. User must be logged in. @@ -198,7 +198,7 @@ def get_projects( library: Optional[bool] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Generator["ProjectDict", None, None]: + ) -> Generator[ProjectDict, None, None]: """Get projects. Args: @@ -244,7 +244,7 @@ def get_project( project_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Optional["ProjectDict"]: + ) -> Optional[ProjectDict]: """Get project. Args: @@ -287,7 +287,7 @@ def create_project( project_code: str, library_project: bool = False, preset_name: Optional[str] = None, - ) -> "ProjectDict": + ) -> ProjectDict: """Create project using AYON settings. This project creation function is not validating project entity on @@ -359,7 +359,7 @@ def update_project( active: Optional[bool] = None, project_code: Optional[str] = None, **changes - ): + ) -> None: """Update project entity on server. Args: @@ -669,7 +669,7 @@ def _get_graphql_projects( fields: set[str], own_attributes: bool, project_name: Optional[str] = None - ): + ) -> Generator[ProjectDict, None, None]: if active is not None: fields.add("active") diff --git a/ayon_api/_api_helpers/representations.py b/ayon_api/_api_helpers/representations.py index e46353d09..2eb9814cb 100644 --- a/ayon_api/_api_helpers/representations.py +++ b/ayon_api/_api_helpers/representations.py @@ -26,7 +26,7 @@ class RepresentationsAPI(BaseServerAPI): def get_rest_representation( self, project_name: str, representation_id: str - ) -> Optional["RepresentationDict"]: + ) -> Optional[RepresentationDict]: return self.get_rest_entity_by_id( project_name, "representation", representation_id ) @@ -44,7 +44,7 @@ def get_representations( has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Generator["RepresentationDict", None, None]: + ) -> Generator[RepresentationDict, None, None]: """Get representation entities based on passed filters from server. .. todo:: @@ -182,7 +182,7 @@ def get_representation_by_id( representation_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Optional["RepresentationDict"]: + ) -> Optional[RepresentationDict]: """Query representation entity from server based on id filter. Args: @@ -216,7 +216,7 @@ def get_representation_by_name( version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Optional["RepresentationDict"]: + ) -> Optional[RepresentationDict]: """Query representation entity by name and version id. Args: @@ -474,7 +474,7 @@ def get_representation_parents( folder_fields: Optional[Iterable[str]] = None, product_fields: Optional[Iterable[str]] = None, version_fields: Optional[Iterable[str]] = None, - ) -> Optional["RepresentationParents"]: + ) -> Optional[RepresentationParents]: """Find representation parents by representation id. Representation parent entities up to project. @@ -668,7 +668,7 @@ def update_representation( tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - ): + ) -> None: """Update representation entity on server. Update of ``data`` will override existing value on folder entity. @@ -714,7 +714,7 @@ def update_representation( def delete_representation( self, project_name: str, representation_id: str - ): + ) -> None: """Delete representation. Args: @@ -728,8 +728,8 @@ def delete_representation( response.raise_for_status() def _representation_conversion( - self, representation: "RepresentationDict" - ): + self, representation: RepresentationDict + ) -> None: if "context" in representation: orig_context = representation["context"] context = {} diff --git a/ayon_api/_api_helpers/secrets.py b/ayon_api/_api_helpers/secrets.py index f02649fef..dbc9084bb 100644 --- a/ayon_api/_api_helpers/secrets.py +++ b/ayon_api/_api_helpers/secrets.py @@ -8,7 +8,7 @@ class SecretsAPI(BaseServerAPI): - def get_secrets(self) -> list["SecretDict"]: + def get_secrets(self) -> list[SecretDict]: """Get all secrets. Example output:: @@ -32,7 +32,7 @@ def get_secrets(self) -> list["SecretDict"]: response.raise_for_status() return response.data - def get_secret(self, secret_name: str) -> "SecretDict": + def get_secret(self, secret_name: str) -> SecretDict: """Get secret by name. Example output:: diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index 7be323c30..aa984032b 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -23,7 +23,7 @@ class TasksAPI(BaseServerAPI): def get_rest_task( self, project_name: str, task_id: str - ) -> Optional["TaskDict"]: + ) -> Optional[TaskDict]: return self.get_rest_entity_by_id(project_name, "task", task_id) def get_tasks( @@ -40,7 +40,7 @@ def get_tasks( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False - ) -> Generator["TaskDict", None, None]: + ) -> Generator[TaskDict, None, None]: """Query task entities from server. Args: @@ -122,7 +122,7 @@ def get_task_by_name( task_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, - ) -> Optional["TaskDict"]: + ) -> Optional[TaskDict]: """Query task entity by name and folder id. Args: @@ -156,7 +156,7 @@ def get_task_by_id( task_id: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False - ) -> Optional["TaskDict"]: + ) -> Optional[TaskDict]: """Query task entity by id. Args: @@ -195,7 +195,7 @@ def get_tasks_by_folder_paths( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False - ) -> dict[str, list["TaskDict"]]: + ) -> dict[str, list[TaskDict]]: """Query task entities from server by folder paths. Args: @@ -289,7 +289,7 @@ def get_tasks_by_folder_path( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes: bool = False - ) -> list["TaskDict"]: + ) -> list[TaskDict]: """Query task entities from server by folder path. Args: @@ -337,7 +337,7 @@ def get_task_by_folder_path( task_name: str, fields: Optional[Iterable[str]] = None, own_attributes: bool = False - ) -> Optional["TaskDict"]: + ) -> Optional[TaskDict]: """Query task entity by folder path and task name. Args: @@ -445,7 +445,7 @@ def update_task( status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, - ): + ) -> None: """Update task entity on server. Do not pass ``label`` amd ``thumbnail_id`` if you don't @@ -501,7 +501,7 @@ def update_task( ) response.raise_for_status() - def delete_task(self, project_name: str, task_id: str): + def delete_task(self, project_name: str, task_id: str) -> None: """Delete task. Args: diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 4e2242f2e..e4b1c56d1 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -261,7 +261,7 @@ def create_thumbnail( def update_thumbnail( self, project_name: str, thumbnail_id: str, src_filepath: str - ): + ) -> None: """Change thumbnail content by id. Update can be also used to create new thumbnail. diff --git a/ayon_api/_api_helpers/versions.py b/ayon_api/_api_helpers/versions.py index 1302ed84b..fe8a02469 100644 --- a/ayon_api/_api_helpers/versions.py +++ b/ayon_api/_api_helpers/versions.py @@ -21,7 +21,7 @@ class VersionsAPI(BaseServerAPI): def get_rest_version( self, project_name: str, version_id: str - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: return self.get_rest_entity_by_id(project_name, "version", version_id) def get_versions( @@ -39,7 +39,7 @@ def get_versions( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Generator["VersionDict", None, None]: + ) -> Generator[VersionDict, None, None]: """Get version entities based on passed filters from server. Args: @@ -164,7 +164,7 @@ def get_version_by_id( version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: """Query version entity by id. Args: @@ -200,7 +200,7 @@ def get_version_by_name( product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: """Query version entity by version and product id. Args: @@ -236,7 +236,7 @@ def get_hero_version_by_id( version_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: """Query hero version entity by id. Args: @@ -269,7 +269,7 @@ def get_hero_version_by_product_id( product_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: """Query hero version entity by product id. Only one hero version is available on a product. @@ -306,7 +306,7 @@ def get_hero_versions( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Generator["VersionDict", None, None]: + ) -> Generator[VersionDict, None, None]: """Query hero versions by multiple filters. Only one hero version is available on a product. @@ -346,7 +346,7 @@ def get_last_versions( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> dict[str, Optional["VersionDict"]]: + ) -> dict[str, Optional[VersionDict]]: """Query last version entities by product ids. Args: @@ -391,7 +391,7 @@ def get_last_version_by_product_id( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: """Query last version entity by product id. Args: @@ -429,7 +429,7 @@ def get_last_version_by_product_name( active: Optional[bool] = True, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Optional["VersionDict"]: + ) -> Optional[VersionDict]: """Query last version entity by product name and folder id. Args: @@ -572,7 +572,7 @@ def update_version( status: Optional[str] = None, active: Optional[bool] = None, thumbnail_id: Optional[str] = NOT_SET, - ): + ) -> None: """Update version entity on server. Do not pass ``task_id`` amd ``thumbnail_id`` if you don't @@ -626,7 +626,7 @@ def update_version( ) response.raise_for_status() - def delete_version(self, project_name: str, version_id: str): + def delete_version(self, project_name: str, version_id: str) -> None: """Delete version. Args: diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index 1be1c37a5..e27aab3c1 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -25,7 +25,7 @@ def get_workfiles_info( has_links: Optional[str]=None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Generator["WorkfileInfoDict", None, None]: + ) -> Generator[WorkfileInfoDict, None, None]: """Workfile info entities by passed filters. Args: @@ -121,7 +121,7 @@ def get_workfile_info( path: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Optional["WorkfileInfoDict"]: + ) -> Optional[WorkfileInfoDict]: """Workfile info entity by task id and workfile path. Args: @@ -157,7 +157,7 @@ def get_workfile_info_by_id( workfile_id: str, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, - ) -> Optional["WorkfileInfoDict"]: + ) -> Optional[WorkfileInfoDict]: """Workfile info entity by id. Args: From 5555eb8c0c50af3460d90a412f79962b86effd0c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:48:23 +0200 Subject: [PATCH 164/506] added log property do base --- ayon_api/_api_helpers/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index 7c792d42d..f49b10a9b 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +import logging import typing from typing import Optional, Any, Iterable @@ -18,6 +19,10 @@ class BaseServerAPI: + @property + def log(self) -> logging.Logger: + raise NotImplementedError() + def get_server_version(self) -> str: raise NotImplementedError() From 460e0ce888c1854f06ee70a5a325e99f95975a42 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:48:37 +0200 Subject: [PATCH 165/506] use 'get_server_version_tuple' --- ayon_api/_api_helpers/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index 8f2267f77..c4be8e8f7 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -118,7 +118,7 @@ def get_events( if not fields: fields = self.get_default_fields_for_type("event") - major, minor, patch, _, _ = self.server_version_tuple + major, minor, patch, _, _ = self.get_server_version_tuple() use_states = (major, minor, patch) <= (1, 5, 6) query = events_graphql_query(set(fields), order, use_states) From 2d5fb188cdcc70a176b96bdd826dd68704e3be10 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:48:51 +0200 Subject: [PATCH 166/506] added typehints to save and delete secret --- ayon_api/_api_helpers/secrets.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api_helpers/secrets.py b/ayon_api/_api_helpers/secrets.py index dbc9084bb..188d696c9 100644 --- a/ayon_api/_api_helpers/secrets.py +++ b/ayon_api/_api_helpers/secrets.py @@ -53,7 +53,7 @@ def get_secret(self, secret_name: str) -> SecretDict: response.raise_for_status() return response.data - def save_secret(self, secret_name: str, secret_value: str): + def save_secret(self, secret_name: str, secret_value: str) -> None: """Save secret. This endpoint can create and update secret. @@ -69,9 +69,8 @@ def save_secret(self, secret_name: str, secret_value: str): value=secret_value, ) response.raise_for_status() - return response.data - def delete_secret(self, secret_name: str): + def delete_secret(self, secret_name: str) -> None: """Delete secret by name. Args: @@ -80,4 +79,3 @@ def delete_secret(self, secret_name: str): """ response = self.delete(f"secrets/{secret_name}") response.raise_for_status() - return response.data From 7d5d49e67c0ae2588e2e6f30206c557f210b5b8f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:49:14 +0200 Subject: [PATCH 167/506] better event typehints --- ayon_api/_api_helpers/events.py | 16 ++++++++-------- ayon_api/typing.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index c4be8e8f7..c22ff6200 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -4,7 +4,7 @@ import typing from typing import Optional, Any, Iterable, Generator -from ayon_api.utils import SortOrder, prepare_list_filters +from ayon_api.utils import SortOrder, prepare_list_filters, RestApiResponse from ayon_api.graphql_queries import events_graphql_query from .base import BaseServerAPI @@ -12,7 +12,7 @@ if typing.TYPE_CHECKING: from typing import Union - from ayon_api.typing import EventFilter + from ayon_api.typing import EventFilter, EventStatus, EnrollEventData class EventsAPI(BaseServerAPI): @@ -38,7 +38,7 @@ def get_events( topics: Optional[Iterable[str]] = None, event_ids: Optional[Iterable[str]] = None, project_names: Optional[Iterable[str]] = None, - statuses: Optional[Iterable[str]] = None, + statuses: Optional[Iterable[EventStatus]] = None, users: Optional[Iterable[str]] = None, include_logs: Optional[bool] = None, has_children: Optional[bool] = None, @@ -59,7 +59,7 @@ def get_events( event_ids (Optional[Iterable[str]]): Event ids. project_names (Optional[Iterable[str]]): Project on which event happened. - statuses (Optional[Iterable[str]]): Filtering by statuses. + statuses (Optional[Iterable[EventStatus]]): Filtering by statuses. users (Optional[Iterable[str]]): Filtering by users who created/triggered an event. include_logs (Optional[bool]): Query also log events. @@ -139,7 +139,7 @@ def update_event( sender: Optional[str] = None, project_name: Optional[str] = None, username: Optional[str] = None, - status: Optional[str] = None, + status: Optional[EventStatus] = None, description: Optional[str] = None, summary: Optional[dict[str, Any]] = None, payload: Optional[dict[str, Any]] = None, @@ -153,7 +153,7 @@ def update_event( sender (Optional[str]): New sender of event. project_name (Optional[str]): New project name. username (Optional[str]): New username. - status (Optional[str]): New event status. Enum: "pending", + status (Optional[EventStatus]): New event status. Enum: "pending", "in_progress", "finished", "failed", "aborted", "restarted" description (Optional[str]): New description. summary (Optional[dict[str, Any]]): New summary. @@ -282,7 +282,7 @@ def enroll_event_job( max_retries: Optional[int] = None, ignore_older_than: Optional[str] = None, ignore_sender_types: Optional[str] = None, - ): + ) -> Optional[EnrollEventData]: """Enroll job based on events. Enroll will find first unprocessed event with 'source_topic' and will @@ -337,7 +337,7 @@ def enroll_event_job( by given sender types. Returns: - Optional[dict[str, Any]]: None if there is no event matching + Optional[EnrollEventData]: None if there is no event matching filters. Created event with 'target_topic'. """ diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 9f45a5120..aa31065ea 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -342,6 +342,22 @@ class SecretDict(TypedDict): ActivityDict, ] +EventStatus = Literal[ + "pending", + "in_progress", + "finished", + "failed", + "aborted", + "restarted", +] + + +class EnrollEventData(TypedDict): + id: str + dependsOn: str + hash: str + status: EventStatus + class FlatFolderDict(TypedDict): id: str From 7b362b73f27aecf382100617902b7b78f055e3cb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:52:16 +0200 Subject: [PATCH 168/506] added 'create_event' function that returns event id --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 56 +++++++++++++++++++++++++++++++++ ayon_api/_api_helpers/events.py | 56 +++++++++++++++++++++++++++++++++ 3 files changed, 114 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 1d28e6736..a51f12bc4 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -134,6 +134,7 @@ get_events, update_event, dispatch_event, + create_event, delete_event, enroll_event_job, get_attributes_schema, @@ -398,6 +399,7 @@ "get_events", "update_event", "dispatch_event", + "create_event", "delete_event", "enroll_event_job", "get_attributes_schema", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 24a1bda94..435dd3a0e 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -3104,6 +3104,62 @@ def dispatch_event( ) +def create_event( + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, +) -> str: + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + str: Event id. + + """ + con = get_server_api_connection() + return con.create_event( + topic=topic, + sender=sender, + event_hash=event_hash, + project_name=project_name, + username=username, + depends_on=depends_on, + description=description, + summary=summary, + payload=payload, + finished=finished, + store=store, + dependencies=dependencies, + ) + + def delete_event( event_id: str, ) -> None: diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index c22ff6200..19e12c6b8 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -256,6 +256,62 @@ def dispatch_event( response.raise_for_status() return response + def create_event( + self, + topic: str, + sender: Optional[str] = None, + event_hash: Optional[str] = None, + project_name: Optional[str] = None, + username: Optional[str] = None, + depends_on: Optional[str] = None, + description: Optional[str] = None, + summary: Optional[dict[str, Any]] = None, + payload: Optional[dict[str, Any]] = None, + finished: bool = True, + store: bool = True, + dependencies: Optional[list[str]] = None, + ) -> str: + """Dispatch event to server. + + Args: + topic (str): Event topic used for filtering of listeners. + sender (Optional[str]): Sender of event. + event_hash (Optional[str]): Event hash. + project_name (Optional[str]): Project name. + depends_on (Optional[str]): Add dependency to another event. + username (Optional[str]): Username which triggered event. + description (Optional[str]): Description of event. + summary (Optional[dict[str, Any]]): Summary of event that can + be used for simple filtering on listeners. + payload (Optional[dict[str, Any]]): Full payload of event data with + all details. + finished (bool): Mark event as finished on dispatch. + store (bool): Store event in event queue for possible + future processing otherwise is event send only + to active listeners. + dependencies (Optional[list[str]]): Deprecated. + List of event id dependencies. + + Returns: + str: Event id. + + """ + result = self.dispatch_event( + topic, + sender, + event_hash, + project_name, + username, + depends_on, + description, + summary, + payload, + finished, + store, + dependencies, + ) + return result.data["id"] + def delete_event(self, event_id: str) -> None: """Delete event by id. From d7326ad6803a37f11b5aaebc9e3deebd748a28d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 28 Aug 2025 11:57:25 +0200 Subject: [PATCH 169/506] minor typehint fixes --- automated_api.py | 8 ++++---- ayon_api/_api.py | 12 ++++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/automated_api.py b/automated_api.py index db9348686..9565717bd 100644 --- a/automated_api.py +++ b/automated_api.py @@ -136,7 +136,7 @@ def _find_obj(obj_full, api_globals): def _get_typehint(annotation, api_globals): if isinstance(annotation, str): - annotation = annotation.replace("'", '"') + annotation = annotation.replace("'", "") if inspect.isclass(annotation): module_name = str(annotation.__module__) @@ -148,7 +148,7 @@ def _get_typehint(annotation, api_globals): return obj_name print("Unknown typehint:", full_name) - return f'"{full_name}"' + return full_name typehint = ( str(annotation) @@ -213,12 +213,12 @@ def _get_typehint(annotation, api_globals): _typehing_parents.append(parent) if _typehing_parents: - typehint = f'{_typehint}' + typehint = _typehint for parent in reversed(_typehing_parents): typehint = f"{parent}[{typehint}]" return typehint - return f'{typehint}' + return typehint def _get_param_typehint(param, api_globals): diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 435dd3a0e..500955995 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -32,6 +32,7 @@ get_default_settings_variant as _get_default_settings_variant, RepresentationParents, RepresentationHierarchy, + RestApiResponse, ) from .server_api import ( ServerAPI, @@ -49,6 +50,8 @@ EntityListItemMode, LinkDirection, EventFilter, + EventStatus, + EnrollEventData, AttributeScope, AttributeSchemaDataDict, AttributeSchemaDict, @@ -80,6 +83,7 @@ StreamType, EntityListAttributeDefinitionDict, ) + from ._api_helpers.links import CreateLinkData class GlobalServerAPI(ServerAPI): @@ -700,7 +704,7 @@ def get_server_version() -> str: return con.get_server_version() -def get_server_version_tuple() -> "ServerVersion": +def get_server_version_tuple() -> ServerVersion: """Get server version as tuple. Version should match semantic version (https://semver.org/). @@ -908,7 +912,7 @@ def delete( def download_file_to_stream( endpoint: str, - stream: "StreamType", + stream: StreamType, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> TransferProgress: @@ -981,7 +985,7 @@ def download_file( def upload_file_from_stream( endpoint: str, - stream: "StreamType", + stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, **kwargs, @@ -1188,7 +1192,7 @@ def get_rest_entity_by_id( project_name: str, entity_type: str, entity_id: str, -) -> Optional["AnyEntityDict"]: +) -> Optional[AnyEntityDict]: """Get entity using REST on a project by its id. Args: From c5a6a94ca4e5ae08aed76ad1bd14602560c6130e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 29 Aug 2025 13:48:50 +0200 Subject: [PATCH 170/506] don't wrap ServerAPI --- ayon_api/operations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 96a7d6335..1192c290a 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -436,7 +436,7 @@ def session(self) -> OperationsSession: return self._session @property - def con(self) -> "ServerAPI": + def con(self) -> ServerAPI: return self.session.con def to_data(self) -> dict[str, Any]: @@ -644,7 +644,7 @@ class OperationsSession(object): is used if not passed. """ - def __init__(self, con: Optional["ServerAPI"] = None) -> None: + def __init__(self, con: Optional[ServerAPI] = None) -> None: if con is None: con = get_server_api_connection() self._con = con @@ -653,7 +653,7 @@ def __init__(self, con: Optional["ServerAPI"] = None) -> None: self._nested_operations = collections.defaultdict(list) @property - def con(self) -> "ServerAPI": + def con(self) -> ServerAPI: return self._con def get_project( From 070095e4335ed61ad4e8560ea3bb95c48c49668d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Sep 2025 11:46:35 +0200 Subject: [PATCH 171/506] added typed dictionaries for new entities --- ayon_api/operations.py | 27 ++++++++++++------- ayon_api/typing.py | 59 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 10 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 1192c290a..a9a4074d7 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -13,6 +13,13 @@ if typing.TYPE_CHECKING: from .server_api import ServerAPI + from .typing import ( + NewFolderDict, + NewProductDict, + NewVersionDict, + NewRepresentationDict, + NewWorkfileDict, + ) def _create_or_convert_to_id(entity_id: Optional[str] = None) -> str: @@ -74,7 +81,7 @@ def new_folder_entity( data: Optional[dict[str, Any]] = None, thumbnail_id: Optional[str] = None, entity_id: Optional[str] = None -) -> dict[str, Any]: +) -> NewFolderDict: """Create skeleton data of folder entity. Args: @@ -130,7 +137,7 @@ def new_product_entity( attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, entity_id: Optional[str] = None, -) -> dict[str, Any]: +) -> NewProductDict: """Create skeleton data of product entity. Args: @@ -148,7 +155,7 @@ def new_product_entity( created if not passed. Returns: - dict[str, Any]: Skeleton of product entity. + NewProductDict: Skeleton of product entity. """ if attribs is None: @@ -183,7 +190,7 @@ def new_version_entity( attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, entity_id: Optional[str] = None, -) -> dict[str, Any]: +) -> NewVersionDict: """Create skeleton data of version entity. Args: @@ -202,7 +209,7 @@ def new_version_entity( created if not passed. Returns: - dict[str, Any]: Skeleton of version entity. + NewVersionDict: Skeleton of version entity. """ if attribs is None: @@ -242,7 +249,7 @@ def new_hero_version_entity( attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, entity_id: Optional[str] = None, -) -> dict[str, Any]: +) -> NewVersionDict: """Create skeleton data of hero version entity. Args: @@ -261,7 +268,7 @@ def new_hero_version_entity( created if not passed. Returns: - dict[str, Any]: Skeleton of version entity. + NewVersionDict: Skeleton of version entity. """ return new_version_entity( @@ -288,7 +295,7 @@ def new_representation_entity( data: Optional[dict[str, Any]] = None, traits: Optional[dict[str, Any]] = None, entity_id: Optional[str] = None, -) -> dict[str, Any]: +) -> NewRepresentationDict: """Create skeleton data of representation entity. Args: @@ -307,7 +314,7 @@ def new_representation_entity( if not passed. Returns: - dict[str, Any]: Skeleton of representation entity. + NewRepresentationDict: Skeleton of representation entity. """ if attribs is None: @@ -342,7 +349,7 @@ def new_workfile_info( description: Optional[str] = None, data: Optional[dict[str, Any]] = None, entity_id: Optional[str] = None, -) -> dict[str, Any]: +) -> NewWorkfileDict: """Create skeleton data of workfile info entity. Workfile entity is at this moment used primarily for artist notes. diff --git a/ayon_api/typing.py b/ayon_api/typing.py index aa31065ea..8957cf92b 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -342,6 +342,65 @@ class SecretDict(TypedDict): ActivityDict, ] + +class NewFolderDict(TypedDict): + id: str + name: str + folderType: str + parentId: Optional[str] + data: dict[str, Any] + attrib: dict[str, Any] + thumbnailId: Optional[str] + status: Optional[str] + tags: Optional[list[str]] + + +class NewProductDict(TypedDict): + id: str + name: str + productType: str + folderId: str + data: dict[str, Any] + attrib: dict[str, Any] + status: Optional[str] + tags: Optional[list[str]] + + +class NewVersionDict(TypedDict): + id: str + version: int + productId: str + attrib: dict[str, Any] + data: dict[str, Any] + taskId: Optional[str] + thumbnailId: Optional[str] + author: Optional[str] + status: Optional[str] + tags: Optional[list[str]] + + +class NewRepresentationDict(TypedDict): + id: str + versionId: str + name: str + data: dict[str, Any] + attrib: dict[str, Any] + files: list[dict[str, str]] + traits: Optional[dict[str, Any]] + status: Optional[str] + tags: Optional[list[str]] + + +class NewWorkfileDict(TypedDict): + id: str + taskId: str + path: str + data: dict[str, Any] + attrib: dict[str, Any] + status: Optional[str] + tags: Optional[list[str]] + + EventStatus = Literal[ "pending", "in_progress", From 970c332e77df97305fcc7a103c4c9b2e3589e7ca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:20:50 +0200 Subject: [PATCH 172/506] graphql has typehints --- ayon_api/graphql.py | 272 +++++++++++++++++++++++++------------------- 1 file changed, 156 insertions(+), 116 deletions(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index bd6a64efe..da98cb3c9 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -1,15 +1,23 @@ +from __future__ import annotations + import copy import numbers from abc import ABC, abstractmethod -from typing import Optional, Iterable +import typing +from typing import Optional, Iterable, Any, Generator + +from ayon_api import ServerAPI from .exceptions import GraphQlQueryFailed from .utils import SortOrder +if typing.TYPE_CHECKING: + from typing import Union + FIELD_VALUE = object() -def fields_to_dict(fields): +def fields_to_dict(fields: Optional[Iterable[str]]) -> dict: output = {} if not fields: return output @@ -31,7 +39,7 @@ def fields_to_dict(fields): return output -class QueryVariable(object): +class QueryVariable: """Object representing single varible used in GraphQlQuery. Variable definition is in GraphQl query header but it's value is used @@ -41,28 +49,27 @@ class QueryVariable(object): variable_name (str): Name of variable in query. """ - - def __init__(self, variable_name): + def __init__(self, variable_name: str) -> None: self._variable_name = variable_name - self._name = "${}".format(variable_name) + self._name = f"${variable_name}" @property - def name(self): + def name(self) -> str: """Name used in field filter.""" return self._name @property - def variable_name(self): + def variable_name(self) -> str: """Name of variable in query definition.""" return self._variable_name def __hash__(self): return self._name.__hash__() - def __str__(self): + def __str__(self) -> str: return self._name - def __format__(self, *args, **kwargs): + def __format__(self, *args, **kwargs) -> str: return self._name.__format__(*args, **kwargs) @@ -78,7 +85,7 @@ class GraphQlQuery: """ offset = 2 - def __init__(self, name, order=None): + def __init__(self, name: str, order: Optional[int] = None) -> None: self._name = name self._variables = {} self._children = [] @@ -86,7 +93,7 @@ def __init__(self, name, order=None): self._order = SortOrder.parse_value(order, SortOrder.ascending) @property - def indent(self): + def indent(self) -> int: """Indentation for preparation of query string. Returns: @@ -96,7 +103,7 @@ def indent(self): return 0 @property - def child_indent(self): + def child_indent(self) -> int: """Indentation for preparation of query string used by children. Returns: @@ -106,7 +113,7 @@ def child_indent(self): return self.indent @property - def need_query(self): + def need_query(self) -> bool: """Still need query from server. Needed for edges which use pagination. @@ -121,7 +128,7 @@ def need_query(self): return False @property - def has_multiple_edge_fields(self): + def has_multiple_edge_fields(self) -> bool: if self._has_multiple_edge_fields is None: edge_counter = 0 for child in self._children: @@ -132,7 +139,9 @@ def has_multiple_edge_fields(self): return self._has_multiple_edge_fields - def add_variable(self, key, value_type, value=None): + def add_variable( + self, key: str, value_type: str, value: Optional[Any] = None + ) -> QueryVariable: """Add variable to query. Args: @@ -163,7 +172,7 @@ def add_variable(self, key, value_type, value=None): } return variable - def get_variable(self, key): + def get_variable(self, key: str) -> QueryVariable: """Variable object. Args: @@ -175,7 +184,9 @@ def get_variable(self, key): """ return self._variables[key]["variable"] - def get_variable_value(self, key, default=None): + def get_variable_value( + self, key: str, default: Optional[Any] = None + ) -> Any: """Get Current value of variable. Args: @@ -191,7 +202,7 @@ def get_variable_value(self, key, default=None): return variable_item["value"] return default - def set_variable_value(self, key, value): + def set_variable_value(self, key: str, value: Any) -> None: """Set value for variable. Args: @@ -201,7 +212,7 @@ def set_variable_value(self, key, value): """ self._variables[key]["value"] = value - def get_variable_keys(self): + def get_variable_keys(self) -> set[str]: """Get all variable keys. Returns: @@ -210,13 +221,13 @@ def get_variable_keys(self): """ return set(self._variables.keys()) - def get_variables_values(self): + def get_variables_values(self) -> dict[str, Any]: """Calculate variable values used that should be used in query. Variables with value set to 'None' are skipped. Returns: - Dict[str, Any]: Variable values by their name. + dict[str, Any]: Variable values by their name. """ output = {} @@ -227,7 +238,7 @@ def get_variables_values(self): return output - def add_obj_field(self, field): + def add_obj_field(self, field: BaseGraphQlQueryField) -> None: """Add field object to children. Args: @@ -240,7 +251,7 @@ def add_obj_field(self, field): self._children.append(field) field.set_parent(self) - def add_field_with_edges(self, name): + def add_field_with_edges(self, name: str) -> GraphQlQueryEdgeField: """Add field with edges to query. Args: @@ -254,7 +265,7 @@ def add_field_with_edges(self, name): self.add_obj_field(item) return item - def add_field(self, name): + def add_field(self, name: str) -> GraphQlQueryField: """Add field to query. Args: @@ -270,7 +281,7 @@ def add_field(self, name): def get_field_by_keys( self, keys: Iterable[str] - ) -> Optional["BaseGraphQlQueryField"]: + ) -> Optional[BaseGraphQlQueryField]: keys = list(keys) if not keys: return None @@ -283,10 +294,10 @@ def get_field_by_keys( def get_field_by_path( self, path: str - ) -> Optional["BaseGraphQlQueryField"]: + ) -> Optional[BaseGraphQlQueryField]: return self.get_field_by_keys(path.split("/")) - def calculate_query(self): + def calculate_query(self) -> str: """Calculate query string which is sent to server. Returns: @@ -304,14 +315,12 @@ def calculate_query(self): if item["value"] is None: continue - variables.append( - "{}: {}".format(item["variable"], item["type"]) - ) + variables.append(f"{item['variable']}: {item['type']}") variables_str = "" if variables: - variables_str = "({})".format(",".join(variables)) - header = "query {}{}".format(self._name, variables_str) + variables_str = f"({','.join(variables)})" + header = f"query {self._name}{variables_str}" output = [] output.append(header + " {") @@ -321,16 +330,21 @@ def calculate_query(self): return "\n".join(output) - def parse_result(self, data, output, progress_data): + def parse_result( + self, + data: dict[str, Any], + output: dict[str, Any], + progress_data: dict[str, Any], + ) -> None: """Parse data from response for output. Output is stored to passed 'output' variable. That's because of paging during which objects must have access to both new and previous values. Args: - data (Dict[str, Any]): Data received using calculated query. - output (Dict[str, Any]): Where parsed data are stored. - progress_data (Dict[str, Any]): Data used for paging. + data (dict[str, Any]): Data received using calculated query. + output (dict[str, Any]): Where parsed data are stored. + progress_data (dict[str, Any]): Data used for paging. """ if not data: @@ -339,14 +353,14 @@ def parse_result(self, data, output, progress_data): for child in self._children: child.parse_result(data, output, progress_data) - def query(self, con): + def query(self, con: ServerAPI) -> dict[str, Any]: """Do a query from server. Args: con (ServerAPI): Connection to server with 'query' method. Returns: - Dict[str, Any]: Parsed output from GraphQl query. + dict[str, Any]: Parsed output from GraphQl query. """ progress_data = {} @@ -364,14 +378,16 @@ def query(self, con): return output - def continuous_query(self, con): + def continuous_query( + self, con: ServerAPI + ) -> Generator[dict[str, Any], None, None]: """Do a query from server. Args: con (ServerAPI): Connection to server with 'query' method. Returns: - Dict[str, Any]: Parsed output from GraphQl query. + dict[str, Any]: Parsed output from GraphQl query. """ progress_data = {} @@ -414,7 +430,12 @@ class BaseGraphQlQueryField(ABC): field. """ - def __init__(self, name, parent, order): + def __init__( + self, + name: str, + parent: Union[BaseGraphQlQueryField, GraphQlQuery], + order: SortOrder, + ): if isinstance(parent, GraphQlQuery): query_item = parent else: @@ -438,14 +459,16 @@ def __init__(self, name, parent, order): self._fetched_counter = 0 def __repr__(self): - return "<{} {}>".format(self.__class__.__name__, self.path) + return f"<{self.__class__.__name__} {self.path}>" def get_name(self) -> str: return self._name name = property(get_name) - def get_field_by_keys(self, keys: Iterable[str]): + def get_field_by_keys( + self, keys: Iterable[str] + ) -> Optional[BaseGraphQlQueryField]: keys = list(keys) if not keys: return self @@ -456,10 +479,10 @@ def get_field_by_keys(self, keys: Iterable[str]): return child.get_field_by_keys(keys) return None - def set_limit(self, limit: Optional[int]): + def set_limit(self, limit: Optional[int]) -> None: self._limit = limit - def set_order(self, order): + def set_order(self, order: SortOrder) -> None: order = SortOrder.parse_value(order) if order is None: raise ValueError( @@ -468,15 +491,20 @@ def set_order(self, order): ) self._order = order - def set_ascending_order(self, enabled=True): + def set_ascending_order(self, enabled: bool = True) -> None: self.set_order( SortOrder.ascending if enabled else SortOrder.descending ) - def set_descending_order(self, enabled=True): + def set_descending_order(self, enabled: bool = True) -> None: self.set_ascending_order(not enabled) - def add_variable(self, key, value_type, value=None): + def add_variable( + self, + key: str, + value_type: str, + value: Optional[Any] = None, + ) -> QueryVariable: """Add variable to query. Args: @@ -494,7 +522,7 @@ def add_variable(self, key, value_type, value=None): """ return self._parent.add_variable(key, value_type, value) - def get_variable(self, key): + def get_variable(self, key: str) -> QueryVariable: """Variable object. Args: @@ -507,7 +535,7 @@ def get_variable(self, key): return self._parent.get_variable(key) @property - def need_query(self): + def need_query(self) -> bool: """Still need query from server. Needed for edges which use pagination. Look into children values too. @@ -524,7 +552,7 @@ def need_query(self): return True return False - def _children_iter(self): + def _children_iter(self) -> Generator[BaseGraphQlQueryField, None, None]: """Iterate over all children fields of object. Returns: @@ -534,7 +562,7 @@ def _children_iter(self): for child in self._children: yield child - def sum_edge_fields(self, max_limit=None): + def sum_edge_fields(self, max_limit: Optional[int] = None) -> int: """Check how many edge fields query has. In case there are multiple edge fields or are nested the query can't @@ -559,36 +587,36 @@ def sum_edge_fields(self, max_limit=None): return counter @property - def offset(self): + def offset(self) -> int: return self._query_item.offset @property - def indent(self): + def indent(self) -> int: return self._parent.child_indent + self.offset @property @abstractmethod - def child_indent(self): + def child_indent(self) -> int: pass @property - def query_item(self): + def query_item(self) -> GraphQlQuery: return self._query_item @property @abstractmethod - def has_edges(self): + def has_edges(self) -> bool: pass @property - def child_has_edges(self): + def child_has_edges(self) -> bool: for child in self._children_iter(): if child.has_edges or child.child_has_edges: return True return False @property - def path(self): + def path(self) -> str: """Field path for debugging purposes. Returns: @@ -603,49 +631,53 @@ def path(self): self._path = path return self._path - def reset_cursor(self): + def reset_cursor(self) -> None: for child in self._children_iter(): child.reset_cursor() - def get_variable_value(self, *args, **kwargs): - return self._query_item.get_variable_value(*args, **kwargs) + def get_variable_value( + self, key: str, default: Optional[Any] = None + ) -> Any: + return self._query_item.get_variable_value(key, default) - def set_variable_value(self, *args, **kwargs): - return self._query_item.set_variable_value(*args, **kwargs) + def set_variable_value(self, key: str, value: Any) -> None: + self._query_item.set_variable_value(key, value) - def set_filter(self, key, value): + def set_filter(self, key: str, value: Any) -> None: self._filters[key] = value - def has_filter(self, key): + def has_filter(self, key: str) -> bool: return key in self._filters - def remove_filter(self, key): + def remove_filter(self, key: str) -> None: self._filters.pop(key, None) - def set_parent(self, parent): + def set_parent( + self, parent: Union[BaseGraphQlQueryField, GraphQlQuery] + ) -> None: if self._parent is parent: return self._parent = parent parent.add_obj_field(self) - def add_obj_field(self, field): + def add_obj_field(self, field: BaseGraphQlQueryField) -> None: if field in self._children: return self._children.append(field) field.set_parent(self) - def add_field_with_edges(self, name): + def add_field_with_edges(self, name: str) -> GraphQlQueryEdgeField: item = GraphQlQueryEdgeField(name, self, self._order) self.add_obj_field(item) return item - def add_field(self, name): + def add_field(self, name: str) -> GraphQlQueryField: item = GraphQlQueryField(name, self, self._order) self.add_obj_field(item) return item - def _filter_value_to_str(self, value): + def _filter_value_to_str(self, value: Any) -> Optional[str]: if isinstance(value, QueryVariable): if self.get_variable_value(value.variable_name) is None: return None @@ -655,31 +687,31 @@ def _filter_value_to_str(self, value): return str(value) if isinstance(value, str): - return '"{}"'.format(value) + return f'"{value}"' if isinstance(value, (list, set, tuple)): - return "[{}]".format( - ", ".join( - self._filter_value_to_str(item) - for item in iter(value) - ) + joined_values = ", ".join( + self._filter_value_to_str(item) + for item in iter(value) ) + return f"[{joined_values}]" + raise TypeError( "Unknown type to convert '{}'".format(str(type(value))) ) - def get_filters(self): + def get_filters(self) -> dict[str, Any]: """Receive filters for item. By default just use copy of set filters. Returns: - Dict[str, Any]: Fields filters. + dict[str, Any]: Fields filters. """ return copy.deepcopy(self._filters) - def _filters_to_string(self): + def _filters_to_string(self) -> str: filters = self.get_filters() if not filters: return "" @@ -690,23 +722,29 @@ def _filters_to_string(self): if string_value is None: continue - filter_items.append("{}: {}".format(key, string_value)) + filter_items.append(f"{key}: {string_value}") if not filter_items: return "" - return "({})".format(", ".join(filter_items)) + joined_items = ", ".join(filter_items) + return f"({joined_items})" - def _fake_children_parse(self): + def _fake_children_parse(self) -> None: """Mark children as they don't need query.""" for child in self._children_iter(): child.parse_result({}, {}, {}) @abstractmethod - def calculate_query(self): + def calculate_query(self) -> str: pass @abstractmethod - def parse_result(self, data, output, progress_data): + def parse_result( + self, + data: dict[str, Any], + output: dict[str, Any], + progress_data: dict[str, Any], + ) -> None: pass @@ -714,14 +752,19 @@ class GraphQlQueryField(BaseGraphQlQueryField): has_edges = False @property - def child_indent(self): + def child_indent(self) -> int: return self.indent - def parse_result(self, data, output, progress_data): + def parse_result( + self, + data: dict[str, Any], + output: dict[str, Any], + progress_data: dict[str, Any], + ) -> None: if not isinstance(data, dict): - raise TypeError("{} Expected 'dict' type got '{}'".format( - self._name, str(type(data)) - )) + raise TypeError( + f"{self._name} Expected 'dict' type got '{type(data)}'" + ) self._need_query = False value = data.get(self._name) @@ -763,13 +806,9 @@ def parse_result(self, data, output, progress_data): for child in self._children: child.parse_result(item, item_value, progress_data) - def calculate_query(self): + def calculate_query(self) -> str: offset = self.indent * " " - header = "{}{}{}".format( - offset, - self._name, - self._filters_to_string() - ) + header = f"{offset}{self._name}{self._filters_to_string()}" if not self._children: return header @@ -794,43 +833,48 @@ def __init__(self, *args, **kwargs): self._edge_children = [] @property - def child_indent(self): + def child_indent(self) -> int: offset = self.offset * 2 return self.indent + offset - def _children_iter(self): + def _children_iter(self) -> Generator[BaseGraphQlQueryField, None, None]: for child in super()._children_iter(): yield child for child in self._edge_children: yield child - def add_obj_field(self, field): + def add_obj_field(self, field: BaseGraphQlQueryField) -> None: if field in self._edge_children: return super().add_obj_field(field) - def add_obj_edge_field(self, field): + def add_obj_edge_field(self, field: BaseGraphQlQueryField) -> None: if field in self._edge_children or field in self._children: return self._edge_children.append(field) field.set_parent(self) - def add_edge_field(self, name): - item = GraphQlQueryField(name, self, self._order) + def add_edge_field(self, name: str) -> GraphQlQueryEdgeField: + item = GraphQlQueryEdgeField(name, self, self._order) self.add_obj_edge_field(item) return item - def reset_cursor(self): + def reset_cursor(self) -> None: # Reset cursor only for edges self._cursor = None self._need_query = True super().reset_cursor() - def parse_result(self, data, output, progress_data): + def parse_result( + self, + data: dict[str, Any], + output: dict[str, Any], + progress_data: dict[str, Any], + ) -> None: if not isinstance(data, dict): raise TypeError("{} Expected 'dict' type got '{}'".format( self._name, str(type(data)) @@ -848,13 +892,13 @@ def parse_result(self, data, output, progress_data): node_values = [] output[self._name] = node_values + nodes_by_cursor = {} handle_cursors = self.child_has_edges if handle_cursors: cursor_key = self._get_cursor_key() if cursor_key in progress_data: nodes_by_cursor = progress_data[cursor_key] else: - nodes_by_cursor = {} progress_data[cursor_key] = nodes_by_cursor page_info = value["pageInfo"] @@ -900,10 +944,10 @@ def parse_result(self, data, output, progress_data): child.reset_cursor() self._cursor = new_cursor - def _get_cursor_key(self): - return "{}/__cursor__".format(self.path) + def _get_cursor_key(self) -> str: + return f"{self.path}/__cursor__" - def get_filters(self): + def get_filters(self) -> dict[str, Any]: filters = super().get_filters() limit_key = "first" if self._order == SortOrder.descending: @@ -921,18 +965,14 @@ def get_filters(self): filters["after"] = self._cursor return filters - def calculate_query(self): + def calculate_query(self) -> str: if not self._children and not self._edge_children: raise ValueError("Missing child definitions for edges {}".format( self.path )) offset = self.indent * " " - header = "{}{}{}".format( - offset, - self._name, - self._filters_to_string() - ) + header = f"{offset}{self._name}{self._filters_to_string()}" output = [] output.append(header + " {") From 5b29e900224ae200238213509294677d24d65752 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Sep 2025 12:25:05 +0200 Subject: [PATCH 173/506] fix formatting issue --- ayon_api/graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index da98cb3c9..5ffc8e2d5 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -758,7 +758,7 @@ def child_indent(self) -> int: def parse_result( self, data: dict[str, Any], - output: dict[str, Any], + output: dict[str, Any], progress_data: dict[str, Any], ) -> None: if not isinstance(data, dict): From e73c90698adad716b031b4353a77005bb0140080 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 5 Sep 2025 15:57:26 +0200 Subject: [PATCH 174/506] fix type hints in docstring --- ayon_api/operations.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index a9a4074d7..6780ccfd9 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -921,14 +921,14 @@ def update_folder( folder_id (str): Folder id. name (Optional[str]): New name. folder_type (Optional[str]): New folder type. - parent_id (Optional[Union[str, None]]): New parent folder id. - label (Optional[Union[str, None]]): New label. + parent_id (Optional[str]): New parent folder id. + label (Optional[str]): New label. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + thumbnail_id (Optional[str]): New thumbnail id. Returns: UpdateOperation: Object of update operation. @@ -1074,14 +1074,14 @@ def update_task( name (Optional[str]): New name. task_type (Optional[str]): New task type. folder_id (Optional[str]): New folder id. - label (Optional[Union[str, None]]): New label. + label (Optional[str]): New label. assignees (Optional[str]): New assignees. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + thumbnail_id (Optional[str]): New thumbnail id. Returns: UpdateOperation: Object of update operation. @@ -1350,13 +1350,13 @@ def update_version( version_id (str): Version id. version (Optional[int]): New version. product_id (Optional[str]): New product id. - task_id (Optional[Union[str, None]]): New task id. + task_id (Optional[str]): New task id. attrib (Optional[dict[str, Any]]): New attributes. data (Optional[dict[str, Any]]): New data. tags (Optional[Iterable[str]]): New tags. status (Optional[str]): New status. active (Optional[bool]): New active state. - thumbnail_id (Optional[Union[str, None]]): New thumbnail id. + thumbnail_id (Optional[str]): New thumbnail id. Returns: UpdateOperation: Object of update operation. From f4dfe6625b28ab1d513c9b9fa39d60f07189d910 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 10:57:53 +0200 Subject: [PATCH 175/506] added type hints to entity hub --- ayon_api/entity_hub.py | 910 +++++++++++++++++++++++------------------ 1 file changed, 507 insertions(+), 403 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index ead6b4078..6daa68aac 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1,29 +1,57 @@ +from __future__ import annotations + import re import copy import collections import warnings from abc import ABC, abstractmethod import typing -from typing import Optional, Iterable, Dict, List, Set, Any +from typing import Optional, Iterable, Any, Generator, Type +from .server_api import ServerAPI from ._api import get_server_api_connection from .utils import create_entity_id, convert_entity_id, slugify_string if typing.TYPE_CHECKING: - from typing import Literal, Union + from typing import Literal, Union, TypedDict + + from .typing import ( + AttributeSchemaDict, + FolderDict, + TaskDict, + ProductDict, + VersionDict, + ) StatusState = Literal["not_started", "in_progress", "done", "blocked"] + StatusEntityType = Literal[ + "folder", + "task", + "product", + "version", + "representation", + "workfile" + ] EntityType = Literal["project", "folder", "task", "product", "version"] + AttributeValueType = Union[bool, str, int, float, list[str]] + + class ProjectStatusDict(TypedDict): + name: str + shortName: Optional[str] + state: Optional[StatusState] + icon: Optional[str] + color: Optional[str] + scope: Optional[StatusEntityType] -class _CustomNone(object): - def __init__(self, name=None): +class _CustomNone: + def __init__(self, name: Optional[str] = None) -> None: self._name = name or "CustomNone" - def __repr__(self): - return "<{}>".format(self._name) + def __repr__(self) -> str: + return f"<{self._name}>" - def __bool__(self): + def __bool__(self) -> bool: return False @@ -32,7 +60,7 @@ def __bool__(self): _NOT_SET = _CustomNone("_NOT_SET") -class EntityHub(object): +class EntityHub: """Helper to create, update or remove entities in project. The hub is a guide to operation with folder entities and update of project. @@ -51,8 +79,11 @@ class EntityHub(object): connection (ServerAPI): Connection to server with logged user. """ - - def __init__(self, project_name, connection=None): + def __init__( + self, + project_name: str, + connection: Optional[ServerAPI] = None + ) -> None: if not connection: connection = get_server_api_connection() @@ -66,7 +97,7 @@ def __init__(self, project_name, connection=None): self._path_reset_queue = None @property - def project_name(self): + def project_name(self) -> str: """Project name which is maintained by hub. Returns: @@ -76,7 +107,7 @@ def project_name(self): return self._project_name @property - def project_entity(self): + def project_entity(self) -> ProjectEntity: """Project entity. Returns: @@ -87,7 +118,9 @@ def project_entity(self): self.fill_project_from_server() return self._project_entity - def get_attributes_for_type(self, entity_type: "EntityType"): + def get_attributes_for_type( + self, entity_type: EntityType + ) -> dict[str, AttributeSchemaDict]: """Get attributes available for a type. Attributes are based on entity types. @@ -100,13 +133,13 @@ def get_attributes_for_type(self, entity_type: "EntityType"): be attributes received. Returns: - Dict[str, Dict[str, Any]]: Attribute schemas that are available + dict[str, AttributeSchemaDict]: Attribute schemas that are available for entered entity type. """ return self._connection.get_attributes_for_type(entity_type) - def get_entity_by_id(self, entity_id: str) -> Optional["BaseEntity"]: + def get_entity_by_id(self, entity_id: str) -> Optional[BaseEntity]: """Receive entity by its id without entity type. The entity must be already existing in cached objects. @@ -123,8 +156,8 @@ def get_entity_by_id(self, entity_id: str) -> Optional["BaseEntity"]: def get_folder_by_id( self, entity_id: str, - allow_fetch: Optional[bool] = True, - ) -> Optional["FolderEntity"]: + allow_fetch: bool = True, + ) -> Optional[FolderEntity]: """Get folder entity by id. Args: @@ -143,8 +176,8 @@ def get_folder_by_id( def get_task_by_id( self, entity_id: str, - allow_fetch: Optional[bool] = True, - ) -> Optional["TaskEntity"]: + allow_fetch: bool = True, + ) -> Optional[TaskEntity]: """Get task entity by id. Args: @@ -163,8 +196,8 @@ def get_task_by_id( def get_product_by_id( self, entity_id: str, - allow_fetch: Optional[bool] = True, - ) -> Optional["ProductEntity"]: + allow_fetch: bool = True, + ) -> Optional[ProductEntity]: """Get product entity by id. Args: @@ -183,8 +216,8 @@ def get_product_by_id( def get_version_by_id( self, entity_id: str, - allow_fetch: Optional[bool] = True, - ) -> Optional["VersionEntity"]: + allow_fetch: bool = True, + ) -> Optional[VersionEntity]: """Get version entity by id. Args: @@ -203,8 +236,8 @@ def get_version_by_id( def get_or_fetch_entity_by_id( self, entity_id: str, - entity_types: List["EntityType"], - ): + entity_types: list[EntityType], + ) -> Optional[BaseEntity]: """Get or query entity based on it's id and possible entity types. This is a helper function when entity id is known but entity type may @@ -253,9 +286,7 @@ def get_or_fetch_entity_by_id( fields=self._get_version_fields(), ) else: - raise ValueError( - "Unknown entity type \"{}\"".format(entity_type) - ) + raise ValueError(f"Unknown entity type \"{entity_type}\"") if entity_data: break @@ -268,13 +299,13 @@ def get_or_fetch_entity_by_id( folder_entity.has_published_content = entity_data["hasProducts"] return folder_entity - elif entity_type == "task": + if entity_type == "task": return self.add_task(entity_data) - elif entity_type == "product": + if entity_type == "product": return self.add_product(entity_data) - elif entity_type == "version": + if entity_type == "version": return self.add_version(entity_data) return None @@ -282,8 +313,9 @@ def get_or_fetch_entity_by_id( def get_or_query_entity_by_id( self, entity_id: str, - entity_types: List["EntityType"], - ): + entity_types: list[EntityType], + ) -> Optional[BaseEntity]: + """Get or query entity based on it's id and possible entity types.""" warnings.warn( "Method 'get_or_query_entity_by_id' is deprecated. " "Please use 'get_or_fetch_entity_by_id' instead.", @@ -292,11 +324,12 @@ def get_or_query_entity_by_id( return self.get_or_fetch_entity_by_id(entity_id, entity_types) @property - def entities(self): + def entities(self) -> Generator[BaseEntity, None, None]: """Iterator over available entities. Returns: - Iterator[BaseEntity]: All queried/created entities cached in hub. + Generator[BaseEntity, None, None]: All queried/created entities + cached in hub. """ for entity in self._entities_by_id.values(): @@ -310,30 +343,30 @@ def add_new_folder( label: Optional[str] = None, path: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, - tags: Optional[List[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, - active: bool = UNKNOWN_VALUE, + active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = True, - ): + ) -> FolderEntity: """Create folder object and add it to entity hub. Args: name (str): Name of entity. folder_type (str): Type of folder. Folder type must be available in config of project folder types. - parent_id (Union[str, None]): Id of parent entity. + parent_id (Optional[str]): Id of parent entity. label (Optional[str]): Folder label. path (Optional[str]): Folder path. Path consist of all parent names with slash('/') used as separator. status (Optional[str]): Folder status. - tags (Optional[List[str]]): Folder tags. - attribs (Dict[str, Any]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). - thumbnail_id (Union[str, None]): Id of entity's thumbnail. - active (bool): Is entity active. + tags (Optional[list[str]]): Folder tags. + attribs (dict[str, Any]): Attribute values. + data (dict[str, Any]): Entity data (custom data). + thumbnail_id (Optional[str]): Id of entity's thumbnail. + active (Optional[bool]): Is entity active. entity_id (Optional[str]): Id of the entity. New id is created if not passed. created (Optional[bool]): Entity is new. When 'None' is passed the @@ -370,35 +403,35 @@ def add_new_task( label: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, assignees: Optional[Iterable[str]] = None, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = True, parent_id: Optional[str] = UNKNOWN_VALUE, - ): + ) -> TaskEntity: """Create task object and add it to entity hub. Args: name (str): Name of entity. task_type (str): Type of task. Task type must be available in config of project task types. - folder_id (Union[str, None]): Parent folder id. + folder_id (Optional[str]): Parent folder id. label (Optional[str]): Task label. status (Optional[str]): Task status. tags (Optional[Iterable[str]]): Folder tags. - attribs (Dict[str, Any]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). + attribs (dict[str, Any]): Attribute values. + data (dict[str, Any]): Entity data (custom data). assignees (Optional[Iterable[str]]): User assignees to the task. - thumbnail_id (Union[str, None]): Id of entity's thumbnail. + thumbnail_id (Optional[str]): Id of entity's thumbnail. active (bool): Is entity active. entity_id (Optional[str]): Id of the entity. New id is created if not passed. created (Optional[bool]): Entity is new. When 'None' is passed the value is defined based on value of 'entity_id'. - parent_id (Union[str, None]): DEPRECATED Parent folder id. + parent_id (Optional[str]): DEPRECATED Parent folder id. Returns: TaskEntity: Added task entity. @@ -435,23 +468,23 @@ def add_new_product( self, name: str, product_type: str, - folder_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, + folder_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = True, - ): + ) -> ProductEntity: """Create task object and add it to entity hub. Args: name (str): Name of entity. product_type (str): Type of product. - folder_id (Union[str, None]): Parent folder id. + folder_id (Optional[Union[str, _CustomNone]]): Parent folder id. tags (Optional[Iterable[str]]): Folder tags. - attribs (Dict[str, Any]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). + attribs (dict[str, Any]): Attribute values. + data (dict[str, Any]): Entity data (custom data). active (bool): Is entity active. entity_id (Optional[str]): Id of the entity. New id is created if not passed. @@ -480,28 +513,28 @@ def add_new_product( def add_new_version( self, version: int, - product_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, - task_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, + product_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, + task_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = True, - ): + ) -> VersionEntity: """Create task object and add it to entity hub. Args: version (int): Version. - product_id (Union[str, None]): Parent product id. - task_id (Union[str, None]): Parent task id. + product_id (Union[Union[str, _CustomNone]]): Parent product id. + task_id (Union[Union[str, _CustomNone]]): Parent task id. status (Optional[str]): Task status. tags (Optional[Iterable[str]]): Folder tags. - attribs (Dict[str, Any]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). - thumbnail_id (Union[str, None]): Id of entity's thumbnail. + attribs (dict[str, Any]): Attribute values. + data (dict[str, Any]): Entity data (custom data). + thumbnail_id (Optional[str]): Id of entity's thumbnail. active (bool): Is entity active. entity_id (Optional[str]): Id of the entity. New id is created if not passed. @@ -529,11 +562,11 @@ def add_new_version( self.add_entity(version_entity) return version_entity - def add_folder(self, folder): + def add_folder(self, folder: FolderDict) -> FolderEntity: """Create folder object and add it to entity hub. Args: - folder (Dict[str, Any]): Folder entity data. + folder (FolderDict): Folder entity data. Returns: FolderEntity: Added folder entity. @@ -543,11 +576,11 @@ def add_folder(self, folder): self.add_entity(folder_entity) return folder_entity - def add_task(self, task): + def add_task(self, task: TaskDict) -> TaskEntity: """Create task object and add it to entity hub. Args: - task (Dict[str, Any]): Task entity data. + task (TaskDict): Task entity data. Returns: TaskEntity: Added task entity. @@ -557,11 +590,11 @@ def add_task(self, task): self.add_entity(task_entity) return task_entity - def add_product(self, product): + def add_product(self, product: ProductDict) -> ProductEntity: """Create version object and add it to entity hub. Args: - product (Dict[str, Any]): Version entity data. + product (ProductDict): Version entity data. Returns: ProductEntity: Added version entity. @@ -573,11 +606,11 @@ def add_product(self, product): self.add_entity(product_entity) return product_entity - def add_version(self, version): + def add_version(self, version: VersionDict) -> VersionEntity: """Create version object and add it to entity hub. Args: - version (Dict[str, Any]): Version entity data. + version (dict[str, Any]): Version entity data. Returns: VersionEntity: Added version entity. @@ -589,7 +622,7 @@ def add_version(self, version): self.add_entity(version_entity) return version_entity - def add_entity(self, entity): + def add_entity(self, entity: BaseEntity) -> None: """Add entity to hub cache. Args: @@ -608,7 +641,7 @@ def add_entity(self, entity): if parent is not None: parent.add_child(entity.id) - def folder_path_reseted(self, folder_id): + def folder_path_reseted(self, folder_id: str) -> None: """Method called from 'FolderEntity' on path reset. This should reset cache of folder paths on all children entities. @@ -636,7 +669,7 @@ def folder_path_reseted(self, folder_id): self._path_reset_queue = None - def unset_entity_parent(self, entity_id, parent_id): + def unset_entity_parent(self, entity_id: str, parent_id: str) -> None: entity = self._entities_by_id.get(entity_id) parent = self._entities_by_id.get(parent_id) children_ids = UNKNOWN_VALUE @@ -667,7 +700,12 @@ def unset_entity_parent(self, entity_id, parent_id): new_parent_children.append(entity) self.reset_immutable_for_hierarchy_cache(parent_id) - def set_entity_parent(self, entity_id, parent_id, orig_parent_id=_NOT_SET): + def set_entity_parent( + self, + entity_id: str, + parent_id: str, + orig_parent_id: Optional[str] = _NOT_SET, + ) -> None: parent = self._entities_by_id.get(parent_id) entity = self._entities_by_id.get(entity_id) if entity is None: @@ -706,7 +744,7 @@ def set_entity_parent(self, entity_id, parent_id, orig_parent_id=_NOT_SET): parent.add_child(entity_id) self.reset_immutable_for_hierarchy_cache(parent_id) - def _fetch_entity_children(self, entity): + def _fetch_entity_children(self, entity: BaseEntity) -> None: folder_fields = self._get_folder_fields() task_fields = self._get_task_fields() tasks = [] @@ -761,7 +799,9 @@ def _fetch_entity_children(self, entity): entity.fill_children_ids(children_ids) - def get_entity_children(self, entity, allow_fetch=True): + def get_entity_children( + self, entity: BaseEntity, allow_fetch: bool = True + ) -> Union[list[BaseEntity], Type[UNKNOWN_VALUE]]: children_ids = entity.get_children_ids(allow_fetch=False) if children_ids is not UNKNOWN_VALUE: return entity.get_children() @@ -773,7 +813,7 @@ def get_entity_children(self, entity, allow_fetch=True): return entity.get_children() - def delete_entity(self, entity): + def delete_entity(self, entity: BaseEntity) -> None: parent_id = entity.parent_id if parent_id is None: return @@ -785,8 +825,10 @@ def delete_entity(self, entity): self.unset_entity_parent(entity.id, parent_id) def reset_immutable_for_hierarchy_cache( - self, entity_id: Optional[str], bottom_to_top: Optional[bool] = True - ): + self, + entity_id: Optional[str], + bottom_to_top: Optional[bool] = True, + ) -> None: if bottom_to_top is None or entity_id is None: return @@ -814,7 +856,7 @@ def reset_immutable_for_hierarchy_cache( for child in self._entities_by_parent_id[entity.id]: reset_queue.append(child.id) - def fill_project_from_server(self): + def fill_project_from_server(self) -> ProjectEntity: """Query project data from server and create project entity. This method will invalidate previous object of Project entity. @@ -832,9 +874,7 @@ def fill_project_from_server(self): own_attributes=True ) if not project: - raise ValueError( - "Project \"{}\" was not found.".format(project_name) - ) + raise ValueError(f"Project \"{project_name}\" was not found.") major, minor, _, _, _ = self._connection.get_server_version_tuple() status_scope_supported = True if (major, minor) < (1, 5): @@ -849,7 +889,7 @@ def fill_project_from_server(self): self.add_entity(self._project_entity) return self._project_entity - def _get_folder_fields(self) -> Set[str]: + def _get_folder_fields(self) -> set[str]: folder_fields = set( self._connection.get_default_fields_for_type("folder") ) @@ -857,22 +897,22 @@ def _get_folder_fields(self) -> Set[str]: folder_fields.add("data") return folder_fields - def _get_task_fields(self) -> Set[str]: + def _get_task_fields(self) -> set[str]: return set( self._connection.get_default_fields_for_type("task") ) - def _get_product_fields(self) -> Set[str]: + def _get_product_fields(self) -> set[str]: return set( self._connection.get_default_fields_for_type("product") ) - def _get_version_fields(self) -> Set[str]: + def _get_version_fields(self) -> set[str]: return set( self._connection.get_default_fields_for_type("version") ) - def fetch_hierarchy_entities(self): + def fetch_hierarchy_entities(self) -> None: """Query whole project at once.""" project_entity = self.fill_project_from_server() @@ -928,22 +968,22 @@ def fetch_hierarchy_entities(self): entity = lock_queue.popleft() entity.lock() - def query_entities_from_server(self): + def query_entities_from_server(self) -> None: warnings.warn( "Method 'query_entities_from_server' is deprecated." " Please use 'fetch_hierarchy_entities' instead.", DeprecationWarning ) - return self.fetch_hierarchy_entities() + self.fetch_hierarchy_entities() - def lock(self): + def lock(self) -> None: if self._project_entity is None: return for entity in self._entities_by_id.values(): entity.lock() - def _get_top_entities(self): + def _get_top_entities(self) -> list[BaseEntity]: all_ids = set(self._entities_by_id.keys()) return [ entity @@ -951,7 +991,7 @@ def _get_top_entities(self): if entity.parent_id not in all_ids ] - def _split_entities(self): + def _split_entities(self) -> tuple[list[str], list[str], list[str]]: top_entities = self._get_top_entities() entities_queue = collections.deque(top_entities) removed_entity_ids = [] @@ -973,7 +1013,9 @@ def _split_entities(self): entities_queue.append(child) return created_entity_ids, other_entity_ids, removed_entity_ids - def _get_update_body(self, entity, changes=None): + def _get_update_body( + self, entity: BaseEntity, changes: Optional[dict[str, Any]] = None + ) -> Optional[dict[str, Any]]: if changes is None: changes = entity.changes @@ -986,7 +1028,7 @@ def _get_update_body(self, entity, changes=None): "data": changes } - def _get_create_body(self, entity): + def _get_create_body(self, entity: BaseEntity) -> dict[str, Any]: return { "type": "create", "entityType": entity.entity_type, @@ -994,7 +1036,7 @@ def _get_create_body(self, entity): "data": entity.to_create_body_data() } - def _get_delete_body(self, entity): + def _get_delete_body(self, entity: BaseEntity) -> dict[str, Any]: return { "type": "delete", "entityType": entity.entity_type, @@ -1002,8 +1044,12 @@ def _get_delete_body(self, entity): } def _pre_commit_types_changes( - self, project_changes, orig_types, changes_key, post_changes - ): + self, + project_changes: dict[str, Any], + orig_types: list[dict[str, Any]], + changes_key: Literal["folderType", "taskType"], + post_changes: dict[str, Any], + ) -> None: """Compare changes of types on a project. Compare old and new types. Change project changes content if some old @@ -1043,7 +1089,7 @@ def _pre_commit_types_changes( for type_name in diff_names: new_types.append(orig_types_by_name[type_name]) - def _pre_commit_project(self): + def _pre_commit_project(self) -> dict[str, Any]: """Some project changes cannot be committed before hierarchy changes. It is not possible to change folder types or task types if there are @@ -1077,7 +1123,7 @@ def _pre_commit_project(self): self._connection.update_project(self.project_name, **project_changes) return post_changes - def commit_changes(self): + def commit_changes(self) -> None: """Commit any changes that happened on entities. Todo: @@ -1090,7 +1136,7 @@ def commit_changes(self): project_changes = self.project_entity.changes if project_changes: response = self._connection.patch( - "projects/{}".format(self.project_name), + f"projects/{self.project_name}", **project_changes ) response.raise_for_status() @@ -1163,28 +1209,28 @@ def commit_changes(self): self.lock() -class AttributeValue(object): - def __init__(self, value): +class AttributeValue: + def __init__(self, value: AttributeValueType) -> None: self._value = value self._origin_value = copy.deepcopy(value) - def get_value(self): + def get_value(self) -> AttributeValueType: return self._value - def set_value(self, value): + def set_value(self, value: AttributeValueType) -> None: self._value = value value = property(get_value, set_value) @property - def changed(self): + def changed(self) -> bool: return self._value != self._origin_value - def lock(self): + def lock(self) -> None: self._origin_value = copy.deepcopy(self._value) -class Attributes(object): +class Attributes: """Object representing attribs of entity. Todos: @@ -1194,11 +1240,15 @@ class Attributes(object): Args: attrib_keys (Iterable[str]): Keys that are available in attribs of the entity. - values (Optional[Dict[str, Any]]): Values of attributes. + values (Optional[dict[str, Any]]): Values of attributes. """ - def __init__(self, attrib_keys, values=UNKNOWN_VALUE): + def __init__( + self, + attrib_keys: Iterable[str], + values: Optional[dict[str, Any]] = UNKNOWN_VALUE, + ) -> None: if values in (UNKNOWN_VALUE, None): values = {} self._attributes = { @@ -1206,31 +1256,31 @@ def __init__(self, attrib_keys, values=UNKNOWN_VALUE): for key in attrib_keys } - def __contains__(self, key): + def __contains__(self, key: str) -> bool: return key in self._attributes - def __getitem__(self, key): + def __getitem__(self, key: str) -> AttributeValue: return self._attributes[key].value - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: AttributeValueType) -> None: self._attributes[key].set_value(value) def __iter__(self): for key in self._attributes: yield key - def keys(self): + def keys(self) -> Iterable[str]: return self._attributes.keys() - def values(self): + def values(self) -> Iterable[AttributeValueType]: for attribute in self._attributes.values(): yield attribute.value - def items(self): + def items(self) -> Iterable[tuple[str, AttributeValueType]]: for key, attribute in self._attributes.items(): yield key, attribute.value - def get(self, key, default=None): + def get(self, key: str, default: Optional[Any] = None) -> Any: """Get value of attribute. Args: @@ -1244,17 +1294,17 @@ def get(self, key, default=None): return default return attribute.value - def set(self, key, value): + def set(self, key: str, value: AttributeValueType) -> None: """Change value of attribute. Args: key (str): Attribute name. - value (Any): New value of the attribute. + value (AttributeValueType): New value of the attribute. """ self[key] = value - def get_attribute(self, key): + def get_attribute(self, key: str) -> AttributeValue: """Access to attribute object. Args: @@ -1269,16 +1319,16 @@ def get_attribute(self, key): """ return self._attributes[key] - def lock(self): + def lock(self) -> None: for attribute in self._attributes.values(): attribute.lock() @property - def changes(self): + def changes(self) -> dict[str, AttributeValueType]: """Attribute value changes. Returns: - Dict[str, Any]: Key mapping with new values. + dict[str, Any]: Key mapping with new values. """ return { @@ -1287,7 +1337,7 @@ def changes(self): if attribute.changed } - def to_dict(self, ignore_none=True): + def to_dict(self, ignore_none: bool = True) -> dict[str, Any]: output = {} for key, value in self.items(): if ( @@ -1321,11 +1371,11 @@ class EntityData(dict): } """ - def __init__(self, *args, **kwargs): + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self._orig_data = copy.deepcopy(self) - def get_changes(self): + def get_changes(self) -> dict[str, Any]: """Changes in entity data. Removed keys have value set to 'None'. @@ -1348,11 +1398,11 @@ def get_changes(self): output[key] = self[key] return output - def get_new_entity_value(self): + def get_new_entity_value(self) -> dict[str, AttributeValueType]: """Value of data for new entity. Returns: - dict[str, Any]: Data without None values. + dict[str, AttributeValueType]: Data without None values. """ return { @@ -1362,7 +1412,7 @@ def get_new_entity_value(self): if value is not None } - def lock(self): + def lock(self) -> None: """Lock changes of entity data.""" self._orig_data = copy.deepcopy(self) @@ -1383,8 +1433,8 @@ class BaseEntity(ABC): entity_id (Optional[str]): Entity id. New id is created if not passed. parent_id (Optional[str]): Parent entity id. - attribs (Optional[Dict[str, Any]]): Attribute values. - data (Optional[Dict[str, Any]]): Entity data (custom data). + attribs (Optional[dict[str, Any]]): Attribute values. + data (Optional[dict[str, Any]]): Entity data (custom data). thumbnail_id (Optional[str]): Thumbnail id. active (Optional[bool]): Is entity active. entity_hub (EntityHub): Object of entity hub which created object of @@ -1402,9 +1452,9 @@ class BaseEntity(ABC): def __init__( self, entity_id: Optional[str] = None, - parent_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + parent_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, created: Optional[bool] = None, entity_hub: EntityHub = None, @@ -1412,7 +1462,7 @@ def __init__( name=None, label=None, status: Optional[str] = UNKNOWN_VALUE, - tags: Optional[List[str]] = None, + tags: Optional[list[str]] = None, thumbnail_id: Optional[str] = UNKNOWN_VALUE, ): if entity_hub is None: @@ -1476,12 +1526,12 @@ def __init__( self._immutable_for_hierarchy_cache = None def __repr__(self): - return "<{} - {}>".format(self.__class__.__name__, self.id) + return f"<{self.__class__.__name__} - {self.id}>" - def __getitem__(self, item): + def __getitem__(self, item: str) -> Any: return getattr(self, item) - def __setitem__(self, item, value): + def __setitem__(self, item: str, value: Any) -> None: return setattr(self, item, value) def _prepare_entity_id(self, entity_id: Any) -> str: @@ -1505,11 +1555,11 @@ def removed(self) -> bool: return self._parent_id is None @property - def orig_parent_id(self): + def orig_parent_id(self) -> Optional[str]: return self._orig_parent_id @property - def attribs(self): + def attribs(self) -> Attributes: """Entity attributes based on server configuration. Returns: @@ -1520,7 +1570,7 @@ def attribs(self): return self._attribs @property - def data(self): + def data(self) -> EntityData: """Entity custom data that are not stored by any deterministic model. Be aware that 'data' can't be queried using GraphQl and cannot be @@ -1544,7 +1594,7 @@ def project_name(self) -> str: @property @abstractmethod - def entity_type(self) -> "EntityType": + def entity_type(self) -> EntityType: """Entity type corresponding to server. Returns: @@ -1555,22 +1605,22 @@ def entity_type(self) -> "EntityType": @property @abstractmethod - def parent_entity_types(self) -> List[str]: + def parent_entity_types(self) -> list[str]: """Entity type corresponding to server. Returns: - List[str]: Possible entity types of parent. + list[str]: Possible entity types of parent. """ pass @property @abstractmethod - def changes(self) -> Optional[Dict[str, Any]]: + def changes(self) -> Optional[dict[str, Any]]: """Receive entity changes. Returns: - Optional[Dict[str, Any]]: All values that have changed on + Optional[dict[str, Any]]: All values that have changed on entity. New entity must return None. """ @@ -1579,12 +1629,12 @@ def changes(self) -> Optional[Dict[str, Any]]: @classmethod @abstractmethod def from_entity_data( - cls, entity_data: Dict[str, Any], entity_hub: EntityHub - ) -> "BaseEntity": + cls, entity_data: dict[str, Any], entity_hub: EntityHub + ) -> BaseEntity: """Create entity based on queried data from server. Args: - entity_data (Dict[str, Any]): Entity data from server. + entity_data (dict[str, Any]): Entity data from server. entity_hub (EntityHub): Hub which handle the entity. Returns: @@ -1594,11 +1644,11 @@ def from_entity_data( pass @abstractmethod - def to_create_body_data(self) -> Dict[str, Any]: + def to_create_body_data(self) -> dict[str, Any]: """Convert object of entity to data for server on creation. Returns: - Dict[str, Any]: Entity data. + dict[str, Any]: Entity data. """ pass @@ -1630,7 +1680,7 @@ def immutable_for_hierarchy(self) -> bool: return self._immutable_for_hierarchy_cache @property - def _immutable_for_hierarchy(self): + def _immutable_for_hierarchy(self) -> Optional[bool]: """Override this method to define if entity object is immutable. This property was added to define immutable state of Folder entities @@ -1649,7 +1699,7 @@ def has_cached_immutable_hierarchy(self) -> bool: def reset_immutable_for_hierarchy_cache( self, bottom_to_top: Optional[bool] = True - ): + ) -> None: """Clear cache of immutable hierarchy property. This is used when entity changed parent or a child was added. @@ -1664,11 +1714,11 @@ def reset_immutable_for_hierarchy_cache( self.id, bottom_to_top ) - def _get_default_changes(self): + def _get_default_changes(self) -> dict[str, Any]: """Collect changes of common data on entity. Returns: - Dict[str, Any]: Changes on entity. Key and it's new value. + dict[str, Any]: Changes on entity. Key and it's new value. """ changes = {} @@ -1702,10 +1752,12 @@ def _get_default_changes(self): changes["tags"] = self._tags return changes - def _get_attributes_for_type(self, entity_type): + def _get_attributes_for_type( + self, entity_type: EntityType + ) -> dict[str, AttributeSchemaDict]: return self._entity_hub.get_attributes_for_type(entity_type) - def lock(self): + def lock(self) -> None: """Lock entity as 'saved' so all changes are discarded.""" self._orig_parent_id = self._parent_id self._orig_name = self._name @@ -1726,10 +1778,10 @@ def lock(self): if self._supports_thumbnail: self._orig_thumbnail_id = self._thumbnail_id - def _get_entity_by_id(self, entity_id): + def _get_entity_by_id(self, entity_id: str) -> Optional[BaseEntity]: return self._entity_hub.get_entity_by_id(entity_id) - def get_parent_id(self): + def get_parent_id(self) -> Optional[str]: """Parent entity id. Returns: @@ -1738,7 +1790,7 @@ def get_parent_id(self): """ return self._parent_id - def set_parent_id(self, parent_id): + def set_parent_id(self, parent_id: Optional[str]) -> None: """Change parent by id. Args: @@ -1758,7 +1810,7 @@ def set_parent_id(self, parent_id): parent_id = property(get_parent_id, set_parent_id) - def get_parent(self, allow_fetch=True): + def get_parent(self, allow_fetch: bool = True) -> Optional[BaseEntity]: """Parent entity. Returns: @@ -1779,7 +1831,7 @@ def get_parent(self, allow_fetch=True): self._parent_id, self.parent_entity_types ) - def set_parent(self, parent): + def set_parent(self, parent: BaseEntity) -> None: """Change parent object. Args: @@ -1806,7 +1858,7 @@ def get_children_ids(self, allow_fetch=True): hierarchy. Returns: - Union[List[str], Type[UNKNOWN_VALUE]]: Children iterator. + Union[list[str], Type[UNKNOWN_VALUE]]: Children iterator. """ if self._children_ids is UNKNOWN_VALUE: @@ -1817,11 +1869,13 @@ def get_children_ids(self, allow_fetch=True): children_ids = property(get_children_ids) - def get_children(self, allow_fetch=True): + def get_children( + self, allow_fetch: bool = True + ) -> list[Union[BaseEntity, Type[UNKNOWN_VALUE]]]: """Access to children objects. Returns: - Union[List[BaseEntity], Type[UNKNOWN_VALUE]]: Children iterator. + Union[list[BaseEntity], Type[UNKNOWN_VALUE]]: Children iterator. """ if self._children_ids is UNKNOWN_VALUE: @@ -1836,7 +1890,7 @@ def get_children(self, allow_fetch=True): children = property(get_children) - def add_child(self, child): + def add_child(self, child: Union[BaseEntity, str]) -> None: """Add child entity. Args: @@ -1855,7 +1909,7 @@ def add_child(self, child): self._entity_hub.set_entity_parent(child_id, self.id) - def remove_child(self, child): + def remove_child(self, child: Union[BaseEntity, str]) -> None: """Remove child entity. Is ignored if child is not in children. @@ -1872,7 +1926,7 @@ def remove_child(self, child): self._children_ids.discard(child_id) self._entity_hub.unset_entity_parent(child_id, self.id) - def get_thumbnail_id(self): + def get_thumbnail_id(self) -> str: """Thumbnail id of entity. Returns: @@ -1881,11 +1935,11 @@ def get_thumbnail_id(self): """ return self._thumbnail_id - def set_thumbnail_id(self, thumbnail_id): + def set_thumbnail_id(self, thumbnail_id: Optional[str]) -> None: """Change thumbnail id. Args: - thumbnail_id (Union[str, None]): Thumbnail id for entity. + thumbnail_id (Optional[str]): Thumbnail id for entity. """ self._thumbnail_id = thumbnail_id @@ -1893,7 +1947,7 @@ def set_thumbnail_id(self, thumbnail_id): thumbnail_id = property(get_thumbnail_id, set_thumbnail_id) @property - def created(self): + def created(self) -> bool: """Entity is new. Returns: @@ -1902,7 +1956,7 @@ def created(self): """ return self._created - def fill_children_ids(self, children_ids): + def fill_children_ids(self, children_ids: Iterable[str]) -> None: """Fill children ids on entity. Warning: @@ -1911,14 +1965,14 @@ def fill_children_ids(self, children_ids): """ self._children_ids = set(children_ids) - def get_name(self): + def get_name(self) -> str: if not self._supports_name: raise NotImplementedError( f"Name is not supported for '{self.entity_type}'." ) return self._name - def set_name(self, name): + def set_name(self, name: str) -> None: if not self._supports_name: raise NotImplementedError( f"Name is not supported for '{self.entity_type}'." @@ -1937,14 +1991,14 @@ def get_label(self) -> Optional[str]: ) return self._label - def set_label(self, label: Optional[str]): + def set_label(self, label: Optional[str]) -> None: if not self._supports_label: raise NotImplementedError( f"Label is not supported for '{self.entity_type}'." ) self._label = label - def _get_label_value(self): + def _get_label_value(self) -> Optional[str]: """Get label value that will be used for operations. Returns: @@ -1958,7 +2012,7 @@ def _get_label_value(self): label = property(get_label, set_label) - def get_thumbnail_id(self): + def get_thumbnail_id(self) -> Optional[str]: """Thumbnail id of entity. Returns: @@ -1971,11 +2025,11 @@ def get_thumbnail_id(self): ) return self._thumbnail_id - def set_thumbnail_id(self, thumbnail_id): + def set_thumbnail_id(self, thumbnail_id: Optional[str]) -> None: """Change thumbnail id. Args: - thumbnail_id (Union[str, None]): Thumbnail id for entity. + thumbnail_id (Optional[str]): Thumbnail id for entity. """ if not self._supports_thumbnail: @@ -1986,7 +2040,7 @@ def set_thumbnail_id(self, thumbnail_id): thumbnail_id = property(get_thumbnail_id, set_thumbnail_id) - def get_status(self) -> "Union[str, _CustomNone]": + def get_status(self) -> Union[str, _CustomNone]: """Folder status. Returns: @@ -1999,7 +2053,7 @@ def get_status(self) -> "Union[str, _CustomNone]": ) return self._status - def set_status(self, status_name: str): + def set_status(self, status_name: str) -> None: """Set folder status. Args: @@ -2026,7 +2080,7 @@ def set_status(self, status_name: str): status = property(get_status, set_status) - def get_tags(self): + def get_tags(self) -> list[str]: """Task tags. Returns: @@ -2039,7 +2093,7 @@ def get_tags(self): ) return self._tags - def set_tags(self, tags): + def set_tags(self, tags: Iterable[str]) -> None: """Change tags. Args: @@ -2064,7 +2118,8 @@ class ProjectStatus: state (Optional[StatusState]): A state of the status. icon (Optional[str]): Icon of the status. e.g. 'play_arrow'. color (Optional[str]): Color of the status. e.g. '#eeeeee'. - scope (Optional[Iterable[str]]): Scope of the status. e.g. ['folder']. + scope (Optional[Iterable[StatusEntityType]]): Scope of the + status. e.g. ['folder']. index (Optional[int]): Index of the status. project_statuses (Optional[_ProjectStatuses]): Project statuses wrapper. @@ -2080,16 +2135,16 @@ class ProjectStatus: def __init__( self, - name, - short_name=None, - state=None, - icon=None, - color=None, - scope=None, - index=None, - project_statuses=None, - is_new=None, - ): + name: str, + short_name: Optional[str] = None, + state: Optional[StatusState] = None, + icon: Optional[str] = None, + color: Optional[str] = None, + scope: Optional[StatusEntityType] = None, + index: Optional[int] = None, + project_statuses: Optional[_ProjectStatuses] = None, + is_new: Optional[bool] = None, + ) -> None: short_name = short_name or "" icon = icon or "" state = state or self.default_state @@ -2124,27 +2179,25 @@ def __init__( def __str__(self): short_name = "" if self.short_name: - short_name = "({})".format(self.short_name) - return "<{} {}{}>".format( - self.__class__.__name__, self.name, short_name - ) + short_name = f"({self.short_name})" + return f"<{self.__class__.__name__} {self.name}{short_name}>" - def __repr__(self): + def __repr__(self) -> str: return str(self) - def __getitem__(self, key): + def __getitem__(self, key: str) -> Any: if key in { "name", "short_name", "icon", "state", "color", "slugified_name" }: return getattr(self, key) raise KeyError(key) - def __setitem__(self, key, value): + def __setitem__(self, key: str, value: Any) -> None: if key in {"name", "short_name", "icon", "state", "color"}: return setattr(self, key, value) raise KeyError(key) - def lock(self): + def lock(self) -> None: """Lock status. Changes were commited and current values are now the original values. @@ -2159,13 +2212,13 @@ def lock(self): self._original_scope = self.scope self._original_index = self.index - def is_available_for_entity_type(self, entity_type): + def is_available_for_entity_type(self, entity_type: str) -> bool: if self._scope is None: return True return entity_type in self._scope @staticmethod - def slugify_name(name): + def slugify_name(name: str) -> str: """Slugify status name for name comparison. Args: @@ -2177,7 +2230,7 @@ def slugify_name(name): """ return slugify_string(name.lower()) - def get_project_statuses(self): + def get_project_statuses(self) -> Optional[_ProjectStatuses]: """Internal logic method. Returns: @@ -2186,7 +2239,9 @@ def get_project_statuses(self): """ return self._project_statuses - def set_project_statuses(self, project_statuses): + def set_project_statuses( + self, project_statuses: _ProjectStatuses + ) -> None: """Internal logic method to change parent object. Args: @@ -2195,7 +2250,9 @@ def set_project_statuses(self, project_statuses): """ self._project_statuses = project_statuses - def unset_project_statuses(self, project_statuses): + def unset_project_statuses( + self, project_statuses: _ProjectStatuses + ) -> None: """Internal logic method to unset parent object. Args: @@ -2207,7 +2264,7 @@ def unset_project_statuses(self, project_statuses): self._index = None @property - def changed(self): + def changed(self) -> bool: """Status has changed. Returns: @@ -2225,35 +2282,41 @@ def changed(self): or self._original_scope != self._scope ) - def delete(self): + def delete(self) -> None: """Remove status from project statuses object.""" if self._project_statuses is not None: self._project_statuses.remove(self) - def get_index(self): + def get_index(self) -> Optional[int]: """Get index of status. Returns: - Union[int, None]: Index of status or None if status is not under + Optional[int]: Index of status or None if status is not under project. """ return self._index - def set_index(self, index, **kwargs): + def set_index( + self, + index: Optional[int], + from_parent: bool = False, + ) -> None: """Change status index. Returns: - Union[int, None]: Index of status or None if status is not under + Optional[int]: Index of status or None if status is not under project. + from_parent (bool): For internal usage when called + from '_ProjectStatuses'. """ - if kwargs.get("from_parent"): + if from_parent: self._index = index else: self._project_statuses.set_status_index(self, index) - def get_name(self): + def get_name(self) -> str: """Status name. Returns: @@ -2262,7 +2325,7 @@ def get_name(self): """ return self._name - def set_name(self, name): + def set_name(self, name: str) -> None: """Change status name. Args: @@ -2276,7 +2339,7 @@ def set_name(self, name): self._name = name self._slugified_name = None - def get_short_name(self): + def get_short_name(self) -> str: """Status short name 3 letters tops. Returns: @@ -2285,7 +2348,7 @@ def get_short_name(self): """ return self._short_name - def set_short_name(self, short_name): + def set_short_name(self, short_name: str) -> None: """Change status short name. Args: @@ -2296,7 +2359,7 @@ def set_short_name(self, short_name): raise TypeError("Short name must be a string.") self._short_name = short_name - def get_icon(self): + def get_icon(self) -> str: """Name of icon to use for status. Returns: @@ -2305,7 +2368,7 @@ def get_icon(self): """ return self._icon - def set_icon(self, icon): + def set_icon(self, icon: Optional[str]) -> None: """Change status icon name. Args: @@ -2319,7 +2382,7 @@ def set_icon(self, icon): self._icon = icon @property - def slugified_name(self): + def slugified_name(self) -> str: """Slugified and lowere status name. Can be used for comparison of existing statuses. e.g. 'In Progress' @@ -2333,7 +2396,7 @@ def slugified_name(self): self._slugified_name = self.slugify_name(self.name) return self._slugified_name - def get_state(self): + def get_state(self) -> StatusState: """Get state of project status. Return: @@ -2342,7 +2405,7 @@ def get_state(self): """ return self._state - def set_state(self, state): + def set_state(self, state: StatusState) -> None: """Set color of project status. Args: @@ -2350,10 +2413,10 @@ def set_state(self, state): """ if state not in self.valid_states: - raise ValueError("Invalid state '{}'".format(str(state))) + raise ValueError(f"Invalid state '{state}'") self._state = state - def get_color(self): + def get_color(self) -> str: """Get color of project status. Returns: @@ -2362,7 +2425,7 @@ def get_color(self): """ return self._color - def set_color(self, color): + def set_color(self, color: str) -> None: """Set color of project status. Args: @@ -2371,26 +2434,27 @@ def set_color(self, color): """ if not isinstance(color, str): raise TypeError( - "Color must be string got '{}'".format(type(color))) + f"Color must be string got '{type(color)}'" + ) color = color.lower() if self.color_regex.fullmatch(color) is None: - raise ValueError("Invalid color value '{}'".format(color)) + raise ValueError(f"Invalid color value '{color}'") self._color = color - def get_scope(self): + def get_scope(self) -> set[StatusEntityType]: """Get scope of the status. Returns: - Set[str]: Scope of the status. + set[StatusEntityType]: Scope of the status. """ return set(self._scope) - def set_scope(self, scope): + def set_scope(self, scope: Iterable[StatusEntityType]) -> None: """Get scope of the status. Returns: - scope (Iterable[str]): Scope of the status. + scope (Iterable[StatusEntityType]): Scope of the status. """ if not isinstance(scope, (list, set, tuple)): @@ -2401,9 +2465,8 @@ def set_scope(self, scope): scope = set(scope) invalid_entity_types = scope - self.valid_scope if invalid_entity_types: - raise ValueError("Invalid scope values '{}'".format( - ", ".join(invalid_entity_types) - )) + joined_types = ", ".join(invalid_entity_types) + raise ValueError(f"Invalid scope values '{joined_types}'") self._scope = scope @@ -2416,7 +2479,7 @@ def set_scope(self, scope): icon = property(get_icon, set_icon) scope = property(get_scope, set_scope) - def _validate_other_p_statuses(self, other): + def _validate_other_p_statuses(self, other: ProjectStatus) -> None: """Validate if other status can be used for move. To be able to work with other status, and position them in relation, @@ -2438,15 +2501,16 @@ def _validate_other_p_statuses(self, other): missing_status = self if missing_status is not None: raise ValueError( - "Status '{}' is not assigned to a project.".format( - missing_status.name)) + f"Status '{missing_status.name}' is not assigned" + " to a project." + ) if m_project_statuses is not o_project_statuses: raise ValueError( "Statuse are assigned to different projects." " Cannot execute move." ) - def move_before(self, other): + def move_before(self, other: ProjectStatus) -> None: """Move status before other status. Args: @@ -2456,7 +2520,7 @@ def move_before(self, other): self._validate_other_p_statuses(other) self._project_statuses.set_status_index(self, other.index) - def move_after(self, other): + def move_after(self, other: ProjectStatus) -> None: """Move status after other status. Args: @@ -2466,11 +2530,11 @@ def move_after(self, other): self._validate_other_p_statuses(other) self._project_statuses.set_status_index(self, other.index + 1) - def to_data(self): + def to_data(self) -> dict[str, Any]: """Convert status to data. Returns: - dict[str, str]: Status data. + dict[str, Any]: Status data. """ output = { @@ -2490,13 +2554,18 @@ def to_data(self): return output @classmethod - def from_data(cls, data, index=None, project_statuses=None): + def from_data( + cls, + data: ProjectStatusDict, + index: Optional[int] = None, + project_statuses: Optional[_ProjectStatuses] = None, + ) -> ProjectStatus: """Create project status from data. Args: data (dict[str, str]): Status data. index (Optional[int]): Status index. - project_statuses (Optional[ProjectStatuses]): Project statuses + project_statuses (Optional[_ProjectStatuses]): Project statuses object which wraps the status for a project. """ @@ -2525,7 +2594,7 @@ class _ProjectStatuses: Validate if statuses are duplicated. """ - def __init__(self, statuses): + def __init__(self, statuses: list[ProjectStatusDict]) -> None: self._statuses = [ ProjectStatus.from_data(status, idx, self) for idx, status in enumerate(statuses) @@ -2534,10 +2603,10 @@ def __init__(self, statuses): self._orig_status_length = len(self._statuses) self._set_called = False - def __len__(self): + def __len__(self) -> int: return len(self._statuses) - def __iter__(self): + def __iter__(self) -> Generator[ProjectStatus, None, None]: """Iterate over statuses. Yields: @@ -2549,13 +2618,13 @@ def __iter__(self): def create( self, - name, - short_name=None, - state=None, - icon=None, - color=None, - scope=None, - ): + name: str, + short_name: Optional[str] = None, + state: Optional[StatusState] = None, + icon: Optional[str] = None, + color: Optional[str] = None, + scope: Optional[list[StatusEntityType]] = None, + ) -> ProjectStatus: """Create project status. Args: @@ -2564,7 +2633,8 @@ def create( state (Optional[StatusState]): A state of the status. icon (Optional[str]): Icon of the status. e.g. 'play_arrow'. color (Optional[str]): Color of the status. e.g. '#eeeeee'. - scope (Optional[List[str]]): Scope of the status. e.g. ['folder']. + scope (Optional[list[StatusEntityType]]): Scope of the + status. e.g. ['folder']. Returns: ProjectStatus: Created project status. @@ -2576,10 +2646,10 @@ def create( self.append(status) return status - def set_status_scope_supported(self, supported: bool): + def set_status_scope_supported(self, supported: bool) -> None: self._scope_supported = supported - def lock(self): + def lock(self) -> None: """Lock statuses. Changes were commited and current values are now the original values. @@ -2590,7 +2660,7 @@ def lock(self): for status in self._statuses: status.lock() - def to_data(self): + def to_data(self) -> list[ProjectStatusDict]: """Convert to project statuses data.""" output = [ status.to_data() @@ -2602,13 +2672,13 @@ def to_data(self): item.pop("scope") return output - def set(self, statuses): + def set(self, statuses: list[ProjectStatusDict]) -> None: """Explicitly override statuses. This method does not handle if statuses changed or not. Args: - statuses (list[dict[str, str]]): List of statuses data. + statuses (list[ProjectStatusDict]): List of statuses data. """ self._set_called = True @@ -2618,7 +2688,7 @@ def set(self, statuses): ] @property - def changed(self): + def changed(self) -> bool: """Statuses have changed. Returns: @@ -2638,7 +2708,9 @@ def changed(self): return True return False - def get(self, name, default=None): + def get( + self, name: str, default: Optional[Any] = None + ) -> Union[ProjectStatus, Any]: """Get status by name. Args: @@ -2660,7 +2732,11 @@ def get(self, name, default=None): get_status_by_name = get - def index(self, status, **kwargs): + def index( + self, + status: ProjectStatus, + default: Any = _NOT_SET, + ) -> Union[int, Any]: """Get status index. Args: @@ -2686,18 +2762,20 @@ def index(self, status, **kwargs): if output is not None: return output - if "default" in kwargs: - return kwargs["default"] - raise ValueError("Status '{}' not found".format(status.name)) + if default is _NOT_SET: + raise ValueError(f"Status '{status.name}' not found") + return default - def get_status_by_slugified_name(self, name): + def get_status_by_slugified_name( + self, name: str + ) -> Optional[ProjectStatus]: """Get status by slugified name. Args: name (str): Status name. Is slugified before search. Returns: - Union[ProjectStatus, None]: Status or None if not found. + Optional[ProjectStatus]: Status or None if not found. """ slugified_name = ProjectStatus.slugify_name(name) @@ -2710,7 +2788,9 @@ def get_status_by_slugified_name(self, name): None ) - def remove_by_name(self, name, ignore_missing=False): + def remove_by_name( + self, name: str, ignore_missing: bool = False + ) -> Optional[ProjectStatus]: """Remove status by name. Args: @@ -2719,18 +2799,21 @@ def remove_by_name(self, name, ignore_missing=False): status is not found. Returns: - ProjectStatus: Removed status. + Optional[ProjectStatus]: Removed status. """ matching_status = self.get(name) if matching_status is None: if ignore_missing: - return - raise ValueError( - "Status '{}' not found in project".format(name)) + return None + raise ValueError(f"Status '{name}' not found in project") return self.remove(matching_status) - def remove(self, status, ignore_missing=False): + def remove( + self, + status: ProjectStatus, + ignore_missing: bool = False, + ) -> Optional[ProjectStatus]: """Remove status. Args: @@ -2739,18 +2822,18 @@ def remove(self, status, ignore_missing=False): status is not found. Returns: - Union[ProjectStatus, None]: Removed status. + Optional[ProjectStatus]: Removed status. """ index = self.index(status, default=None) if index is None: if ignore_missing: return None - raise ValueError("Status '{}' not in project".format(status)) + raise ValueError(f"Status '{status}' not in project") return self.pop(index) - def pop(self, index): + def pop(self, index: int) -> ProjectStatus: """Remove status by index. Args: @@ -2766,7 +2849,11 @@ def pop(self, index): st.set_index(st.index - 1, from_parent=True) return status - def insert(self, index, status): + def insert( + self, + index: int, + status: Union[ProjectStatus, ProjectStatusDict], + ) -> ProjectStatus: """Insert status at index. Args: @@ -2778,16 +2865,19 @@ def insert(self, index, status): ProjectStatus: Inserted status. """ - if not isinstance(status, ProjectStatus): - status = ProjectStatus.from_data(status) + matching_index = None + if isinstance(status, ProjectStatus): + p_status: ProjectStatus = status + matching_index = self.index(p_status, default=None) + else: + p_status: ProjectStatus = ProjectStatus.from_data(status) start_index = index end_index = len(self._statuses) + 1 - matching_index = self.index(status, default=None) if matching_index is not None: if matching_index == index: - status.set_index(index, from_parent=True) - return + p_status.set_index(index, from_parent=True) + return p_status self._statuses.pop(matching_index) if matching_index < index: @@ -2796,26 +2886,30 @@ def insert(self, index, status): else: end_index -= 1 - status.set_project_statuses(self) - self._statuses.insert(index, status) + p_status.set_project_statuses(self) + self._statuses.insert(index, p_status) for idx, st in enumerate(self._statuses[start_index:end_index]): st.set_index(start_index + idx, from_parent=True) - return status + return p_status - def append(self, status): + def append( + self, status: Union[ProjectStatus, ProjectStatusDict] + ) -> ProjectStatus: """Add new status to the end of the list. Args: - status (Union[ProjectStatus, dict[str, str]]): Status to insert. - Can be either status object or status data. + status (Union[ProjectStatus, ProjectStatusDict]): Status to + append. Can be either status object or status data. Returns: - ProjectStatus: Inserted status. + ProjectStatus: Appended status. """ return self.insert(len(self._statuses), status) - def set_status_index(self, status, index): + def set_status_index( + self, status: ProjectStatus, index: int + ) -> ProjectStatus: """Set status index. Args: @@ -2835,9 +2929,9 @@ class ProjectEntity(BaseEntity): library (bool): Is project library project. folder_types (list[dict[str, Any]]): Folder types definition. task_types (list[dict[str, Any]]): Task types definition. - statuses: (list[dict[str, Any]]): Statuses definition. - attribs (Optional[Dict[str, Any]]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). + statuses: (list[ProjectStatusDict]): Statuses definition. + attribs (Optional[dict[str, Any]]): Attribute values. + data (dict[str, Any]): Entity data (custom data). active (bool): Is entity active. entity_hub (EntityHub): Object of entity hub which created object of the entity. @@ -2855,11 +2949,11 @@ def __init__( name: str, project_code: str, library: bool, - folder_types: List[Dict[str, Any]], - task_types: List[Dict[str, Any]], - statuses: List[Dict[str, Any]], - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + folder_types: list[dict[str, Any]], + task_types: list[dict[str, Any]], + statuses: list[ProjectStatusDict], + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_hub: EntityHub = None, ): @@ -2886,38 +2980,39 @@ def __init__( self._orig_task_types = copy.deepcopy(task_types) self._orig_statuses = copy.deepcopy(statuses) - def _prepare_entity_id(self, entity_id): + def _prepare_entity_id(self, entity_id: str) -> str: if entity_id != self.project_name: raise ValueError( - "Unexpected entity id value \"{}\". Expected \"{}\"".format( - entity_id, self.project_name)) + f"Unexpected entity id value \"{entity_id}\"." + f" Expected \"{self.project_name}\"" + ) return entity_id - def set_name(self, name): + def set_name(self, name: str) -> None: if self._name == name: return raise ValueError("It is not allowed to change project name.") - def get_parent(self, *args, **kwargs): + def get_parent(self, *args, **kwargs) -> None: return None - def set_parent(self, parent): + def set_parent(self, parent: Any) -> None: raise ValueError( - "Parent of project cannot be set to {}".format(parent) + f"Parent of project cannot be set to {parent}" ) - def set_status_scope_supported(self, supported: bool): + def set_status_scope_supported(self, supported: bool) -> None: self._statuses_obj.set_status_scope_supported(supported) parent = property(get_parent, set_parent) - def get_orig_folder_types(self): + def get_orig_folder_types(self) -> list[dict[str, Any]]: return copy.deepcopy(self._orig_folder_types) - def get_folder_types(self): + def get_folder_types(self) -> list[dict[str, Any]]: return copy.deepcopy(self._folder_types) - def set_folder_types(self, folder_types): + def set_folder_types(self, folder_types: list[dict[str, Any]]) -> None: new_folder_types = [] for folder_type in folder_types: if "icon" not in folder_type: @@ -2925,13 +3020,13 @@ def set_folder_types(self, folder_types): new_folder_types.append(folder_type) self._folder_types = new_folder_types - def get_orig_task_types(self): + def get_orig_task_types(self) -> list[dict[str, Any]]: return copy.deepcopy(self._orig_task_types) - def get_task_types(self): + def get_task_types(self) -> list[dict[str, Any]]: return copy.deepcopy(self._task_types) - def set_task_types(self, task_types): + def set_task_types(self, task_types: list[dict[str, Any]]) -> None: new_task_types = [] for task_type in task_types: if "icon" not in task_type: @@ -2939,20 +3034,20 @@ def set_task_types(self, task_types): new_task_types.append(task_type) self._task_types = new_task_types - def get_orig_statuses(self): + def get_orig_statuses(self) -> list[ProjectStatusDict]: return copy.deepcopy(self._orig_statuses) - def get_statuses(self): + def get_statuses(self) -> _ProjectStatuses: return self._statuses_obj - def set_statuses(self, statuses): + def set_statuses(self, statuses: list[ProjectStatusDict]) -> None: self._statuses_obj.set(statuses) folder_types = property(get_folder_types, set_folder_types) task_types = property(get_task_types, set_task_types) statuses = property(get_statuses, set_statuses) - def get_status_by_slugified_name(self, name): + def get_status_by_slugified_name(self, name: str) -> str: """Find status by name. Args: @@ -2960,19 +3055,19 @@ def get_status_by_slugified_name(self, name): Returns: - Union[ProjectStatus, None]: Status object or None. + Optional[ProjectStatus]: Status object or None. """ return self._statuses_obj.get_status_by_slugified_name(name) - def lock(self): + def lock(self) -> None: super().lock() self._orig_folder_types = copy.deepcopy(self._folder_types) self._orig_task_types = copy.deepcopy(self._task_types) self._statuses_obj.lock() @property - def changes(self): + def changes(self) -> dict[str, Any]: changes = self._get_default_changes() if self._orig_folder_types != self._folder_types: changes["folderTypes"] = self.get_folder_types() @@ -2986,7 +3081,7 @@ def changes(self): return changes @classmethod - def from_entity_data(cls, project, entity_hub) -> "ProjectEntity": + def from_entity_data(cls, project, entity_hub) -> ProjectEntity: return cls( project["name"], project["code"], @@ -3000,7 +3095,7 @@ def from_entity_data(cls, project, entity_hub) -> "ProjectEntity": entity_hub=entity_hub, ) - def to_create_body_data(self): + def to_create_body_data(self) -> dict[str, Any]: raise NotImplementedError( "ProjectEntity does not support conversion to entity data" ) @@ -3013,17 +3108,17 @@ class FolderEntity(BaseEntity): name (str): Name of entity. folder_type (str): Type of folder. Folder type must be available in config of project folder types. - parent_id (Union[str, None]): Id of parent entity. + parent_id (Optional[str]): Id of parent entity. label (Optional[str]): Folder label. path (Optional[str]): Folder path. Path consist of all parent names with slash('/') used as separator. status (Optional[str]): Folder status. - tags (Optional[List[str]]): Folder tags. - attribs (Dict[str, Any]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). - thumbnail_id (Union[str, None]): Id of entity's thumbnail. - active (bool): Is entity active. - entity_id (Union[str, None]): Id of the entity. New id is created if + tags (Optional[list[str]]): Folder tags. + attribs (Optional[dict[str, Any]]): Attribute values. + data (Optional[dict[str, Any]]): Entity data (custom data). + thumbnail_id (Optional[str]): Id of entity's thumbnail. + active (Optional[bool]): Is entity active. + entity_id (Optional[str]): Id of the entity. New id is created if not passed. created (Optional[bool]): Entity is new. When 'None' is passed the value is defined based on value of 'entity_id'. @@ -3048,15 +3143,15 @@ def __init__( label: Optional[str] = None, path: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, - tags: Optional[List[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, - active: bool = UNKNOWN_VALUE, + active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, - ): + ) -> None: super().__init__( entity_id=entity_id, parent_id=parent_id, @@ -3087,35 +3182,34 @@ def __init__( def get_folder_type(self) -> str: return self._folder_type - def set_folder_type(self, folder_type: str): + def set_folder_type(self, folder_type: str) -> None: self._folder_type = folder_type folder_type = property(get_folder_type, set_folder_type) - def get_path(self, dynamic_value=True): + def get_path(self, dynamic_value: bool = True) -> str: if not dynamic_value: return self._path if self._path is None: parent = self.parent if parent.entity_type == "folder": - parent_path = parent.path - path = "/".join([parent_path, self.name]) + path = f"{parent.path}/{self.name}" else: - path = "/{}".format(self.name) + path = f"/{self.name}" self._path = path return self._path - def reset_path(self): + def reset_path(self) -> None: self._path = None self._entity_hub.folder_path_reseted(self.id) path = property(get_path) - def get_has_published_content(self): + def get_has_published_content(self) -> bool: return self._has_published_content - def set_has_published_content(self, has_published_content): + def set_has_published_content(self, has_published_content: bool) -> None: if self._has_published_content is has_published_content: return @@ -3128,17 +3222,17 @@ def set_has_published_content(self, has_published_content): ) @property - def _immutable_for_hierarchy(self): + def _immutable_for_hierarchy(self) -> Optional[bool]: if self.has_published_content: return True return None - def lock(self): + def lock(self) -> None: super().lock() self._orig_folder_type = self._folder_type @property - def changes(self): + def changes(self) -> dict[str, Any]: changes = self._get_default_changes() if self._orig_parent_id != self._parent_id: parent_id = self._parent_id @@ -3152,7 +3246,11 @@ def changes(self): return changes @classmethod - def from_entity_data(cls, folder, entity_hub) -> "FolderEntity": + def from_entity_data( + cls, + folder: dict[str, Any], + entity_hub: EntityHub, + ) -> FolderEntity: parent_id = folder["parentId"] if parent_id is None: parent_id = entity_hub.project_entity.id @@ -3173,7 +3271,7 @@ def from_entity_data(cls, folder, entity_hub) -> "FolderEntity": entity_hub=entity_hub ) - def to_create_body_data(self): + def to_create_body_data(self) -> dict[str, Any]: parent_id = self._parent_id if parent_id is UNKNOWN_VALUE: raise ValueError("Folder does not have set 'parent_id'") @@ -3222,16 +3320,16 @@ class TaskEntity(BaseEntity): name (str): Name of entity. task_type (str): Type of task. Task type must be available in config of project task types. - folder_id (Union[str, None]): Parent folder id. + folder_id (Optional[str]): Parent folder id. label (Optional[str]): Task label. status (Optional[str]): Task status. tags (Optional[Iterable[str]]): Folder tags. - attribs (Dict[str, Any]): Attribute values. - data (Dict[str, Any]): Entity data (custom data). + attribs (dict[str, Any]): Attribute values. + data (dict[str, Any]): Entity data (custom data). assignees (Optional[Iterable[str]]): User assignees to the task. - thumbnail_id (Union[str, None]): Id of entity's thumbnail. - active (bool): Is entity active. - entity_id (Union[str, None]): Id of the entity. New id is created if + thumbnail_id (Optional[str]): Id of entity's thumbnail. + active (Optional[bool]): Is entity active. + entity_id (Optional[str]): Id of the entity. New id is created if not passed. created (Optional[bool]): Entity is new. When 'None' is passed the value is defined based on value of 'entity_id'. @@ -3255,15 +3353,15 @@ def __init__( label: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, assignees: Optional[Iterable[str]] = None, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, - ): + ) -> None: super().__init__( name=name, parent_id=folder_id, @@ -3291,12 +3389,12 @@ def __init__( self._children_ids = set() - def lock(self): + def lock(self) -> None: super().lock() self._orig_task_type = self._task_type self._orig_assignees = copy.deepcopy(self._assignees) - def get_folder_id(self): + def get_folder_id(self) -> Union[str, _CustomNone]: return self._parent_id def set_folder_id(self, folder_id): @@ -3307,12 +3405,12 @@ def set_folder_id(self, folder_id): def get_task_type(self) -> str: return self._task_type - def set_task_type(self, task_type: str): + def set_task_type(self, task_type: str) -> None: self._task_type = task_type task_type = property(get_task_type, set_task_type) - def get_assignees(self): + def get_assignees(self) -> list[str]: """Task assignees. Returns: @@ -3321,7 +3419,7 @@ def get_assignees(self): """ return self._assignees - def set_assignees(self, assignees): + def set_assignees(self, assignees: Iterable[str]) -> None: """Change assignees. Args: @@ -3332,11 +3430,11 @@ def set_assignees(self, assignees): assignees = property(get_assignees, set_assignees) - def add_child(self, child): + def add_child(self, child: BaseEntity) -> None: raise ValueError("Task does not support to add children") @property - def changes(self): + def changes(self) -> dict[str, Any]: changes = self._get_default_changes() if self._orig_parent_id != self._parent_id: @@ -3351,7 +3449,9 @@ def changes(self): return changes @classmethod - def from_entity_data(cls, task, entity_hub) -> "TaskEntity": + def from_entity_data( + cls, task: dict[str, Any], entity_hub: EntityHub + ) -> TaskEntity: return cls( name=task["name"], task_type=task["taskType"], @@ -3369,7 +3469,7 @@ def from_entity_data(cls, task, entity_hub) -> "TaskEntity": entity_hub=entity_hub ) - def to_create_body_data(self): + def to_create_body_data(self) -> dict[str, Any]: if self.parent_id is UNKNOWN_VALUE: raise ValueError("Task does not have set 'parent_id'") @@ -3414,15 +3514,15 @@ def __init__( self, name: str, product_type: str, - folder_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, + folder_id: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, - ): + ) -> None: super().__init__( name=name, parent_id=folder_id, @@ -3438,28 +3538,28 @@ def __init__( self._orig_product_type = product_type - def get_folder_id(self): + def get_folder_id(self) -> Union[str, _CustomNone]: return self._parent_id - def set_folder_id(self, folder_id): + def set_folder_id(self, folder_id: str) -> None: self.set_parent_id(folder_id) folder_id = property(get_folder_id, set_folder_id) - def get_product_type(self): + def get_product_type(self) -> str: return self._product_type - def set_product_type(self, product_type): + def set_product_type(self, product_type: str) -> None: self._product_type = product_type product_type = property(get_product_type, set_product_type) - def lock(self): + def lock(self) -> None: super().lock() self._orig_product_type = self._product_type @property - def changes(self): + def changes(self) -> dict[str, Any]: changes = self._get_default_changes() if self._orig_parent_id != self._parent_id: @@ -3471,7 +3571,9 @@ def changes(self): return changes @classmethod - def from_entity_data(cls, product, entity_hub): + def from_entity_data( + cls, product: dict[str, Any], entity_hub: EntityHub + ) -> ProductEntity: return cls( name=product["name"], product_type=product["productType"], @@ -3485,11 +3587,11 @@ def from_entity_data(cls, product, entity_hub): entity_hub=entity_hub ) - def to_create_body_data(self): + def to_create_body_data(self) -> dict[str, Any]: if self.parent_id is UNKNOWN_VALUE: raise ValueError("Product does not have set 'folder_id'") - output = { + output: dict[str, Any] = { "name": self.name, "productType": self.product_type, "folderId": self.parent_id, @@ -3521,18 +3623,18 @@ class VersionEntity(BaseEntity): def __init__( self, version: int, - product_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, - task_id: Optional["Union[str, _CustomNone]"] = UNKNOWN_VALUE, + product_id: Optional[str] = UNKNOWN_VALUE, + task_id: Optional[str] = UNKNOWN_VALUE, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[Dict[str, Any]] = UNKNOWN_VALUE, - data: Optional[Dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, - ): + ) -> None: super().__init__( parent_id=product_id, status=status, @@ -3551,37 +3653,37 @@ def __init__( self._orig_version = version self._orig_task_id = task_id - def get_version(self): + def get_version(self) -> int: return self._version - def set_version(self, version): + def set_version(self, version: int) -> None: self._version = version version = property(get_version, set_version) - def get_product_id(self): + def get_product_id(self) -> Optional[str]: return self._parent_id - def set_product_id(self, product_id): + def set_product_id(self, product_id: str) -> None: self.set_parent_id(product_id) product_id = property(get_product_id, set_product_id) - def get_task_id(self): + def get_task_id(self) -> Optional[str]: return self._task_id - def set_task_id(self, task_id): + def set_task_id(self, task_id: Optional[str]) -> None: self._task_id = task_id task_id = property(get_task_id, set_task_id) - def lock(self): + def lock(self) -> None: super().lock() self._orig_version = self._version self._orig_task_id = self._task_id @property - def changes(self): + def changes(self) -> dict[str, Any]: changes = self._get_default_changes() if self._orig_parent_id != self._parent_id: @@ -3593,7 +3695,9 @@ def changes(self): return changes @classmethod - def from_entity_data(cls, version, entity_hub): + def from_entity_data( + cls, version: dict[str, Any], entity_hub: EntityHub + ) -> VersionEntity: return cls( version=version["version"], product_id=version["productId"], @@ -3609,11 +3713,11 @@ def from_entity_data(cls, version, entity_hub): entity_hub=entity_hub ) - def to_create_body_data(self): + def to_create_body_data(self) -> dict[str, Any]: if self.parent_id is UNKNOWN_VALUE: raise ValueError("Version does not have set 'product_id'") - output = { + output: dict[str, Any] = { "version": self.version, "productId": self.parent_id, } From 4525b3c7f2a87f4a6486a114fdd540ca3eb4c32b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:39:27 +0200 Subject: [PATCH 176/506] fix line length --- ayon_api/entity_hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 6daa68aac..5bdcc7d62 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -133,8 +133,8 @@ def get_attributes_for_type( be attributes received. Returns: - dict[str, AttributeSchemaDict]: Attribute schemas that are available - for entered entity type. + dict[str, AttributeSchemaDict]: Attribute schemas that + are available for entered entity type. """ return self._connection.get_attributes_for_type(entity_type) From 6ee94cb7f1cd81ab7be9f355812976bb6560ec27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:41:14 +0200 Subject: [PATCH 177/506] fix circular import --- ayon_api/graphql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index 5ffc8e2d5..771752364 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -6,14 +6,14 @@ import typing from typing import Optional, Iterable, Any, Generator -from ayon_api import ServerAPI - from .exceptions import GraphQlQueryFailed from .utils import SortOrder if typing.TYPE_CHECKING: from typing import Union + from .server_api import ServerAPI + FIELD_VALUE = object() From 22cc562070120c7fa8161f942299bd34916b9fa0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:54:36 +0200 Subject: [PATCH 178/506] fix typo in filter variable name fix 'workfilehasLinks' to 'workfileHasLinks' --- ayon_api/_api_helpers/workfiles.py | 2 +- ayon_api/graphql_queries.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index e27aab3c1..0ef9083f5 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -86,7 +86,7 @@ def get_workfiles_info( filters["workfileTags"] = list(tags) if has_links is not None: - filters["workfilehasLinks"] = has_links.upper() + filters["workfileHasLinks"] = has_links.upper() if not fields: fields = self.get_default_fields_for_type("workfile") diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 54648f7d7..38db44672 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -194,7 +194,8 @@ def tasks_graphql_query(fields): tasks_field = project_field.add_field_with_edges("tasks") tasks_field.set_filter("ids", task_ids_var) - # WARNING: At moment when this been created 'names' filter is not supported + # WARNING: At the moment when this been created 'names' filter + # is not supported tasks_field.set_filter("names", task_names_var) tasks_field.set_filter("taskTypes", task_types_var) tasks_field.set_filter("folderIds", folder_ids_var) @@ -241,7 +242,8 @@ def tasks_by_folder_paths_graphql_query(fields): folders_field.set_filter("paths", folder_paths_var) tasks_field = folders_field.add_field_with_edges("tasks") - # WARNING: At moment when this been created 'names' filter is not supported + # WARNING: At the moment when this been created 'names' filter + # is not supported tasks_field.set_filter("names", task_names_var) tasks_field.set_filter("taskTypes", task_types_var) tasks_field.set_filter("assigneesAny", assignees_any_var) @@ -515,7 +517,7 @@ def workfiles_info_graphql_query(fields): task_ids_var = query.add_variable("taskIds", "[String!]") paths_var = query.add_variable("paths", "[String!]") path_regex_var = query.add_variable("workfilePathRegex", "String!") - has_links_var = query.add_variable("workfilehasLinks", "HasLinksFilter") + has_links_var = query.add_variable("workfileHasLinks", "HasLinksFilter") statuses_var = query.add_variable("workfileStatuses", "[String!]") tags_var = query.add_variable("workfileTags", "[String!]") From 633c3a817b5043f59bca07bddda7c4cad76b5bb7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:59:00 +0200 Subject: [PATCH 179/506] fixes of type hints in utils --- ayon_api/utils.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 7f04d64cd..fa9b7ac21 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -12,7 +12,7 @@ import itertools from urllib.parse import urlparse, urlencode import typing -from typing import Optional, Dict, Set, Any, Iterable +from typing import Optional, Any, Iterable, Union from enum import IntEnum import requests @@ -38,7 +38,6 @@ if typing.TYPE_CHECKING: - from typing import Union from .typing import AnyEntityDict, StreamType REMOVED_VALUE = object() @@ -206,7 +205,7 @@ def get(self, key, default=None): return default -def fill_own_attribs(entity: "AnyEntityDict") -> None: +def fill_own_attribs(entity: AnyEntityDict) -> None: """Fill own attributes. Prepare data with own attributes. Prepare data based on a list of @@ -353,7 +352,7 @@ def is_valid(self) -> bool: def prepare_query_string( - key_values: Dict[str, Any], skip_none: bool = True + key_values: dict[str, Any], skip_none: bool = True ) -> str: """Prepare data to query string. @@ -423,7 +422,7 @@ def slugify_string( min_length: int = 1, lower: bool = False, make_set: bool = False, -) -> "Union[str, Set[str]]": +) -> Union[str, set[str]]: """Slugify a text string. This function removes transliterates input string to ASCII, removes @@ -443,7 +442,7 @@ def slugify_string( min_length (int): Minimal length of an element (word). Returns: - Union[str, Set[str]]: Based on 'make_set' value returns slugified + Union[str, set[str]]: Based on 'make_set' value returns slugified string. """ @@ -473,8 +472,8 @@ def failed_json_default(value: Any) -> str: def prepare_attribute_changes( - old_entity: "AnyEntityDict", - new_entity: "AnyEntityDict", + old_entity: AnyEntityDict, + new_entity: AnyEntityDict, replace: int = False, ): attrib_changes = {} @@ -502,10 +501,10 @@ def prepare_attribute_changes( def prepare_entity_changes( - old_entity: "AnyEntityDict", - new_entity: "AnyEntityDict", + old_entity: AnyEntityDict, + new_entity: AnyEntityDict, replace: bool = False, -) -> Dict[str, Any]: +) -> dict[str, Any]: """Prepare changes of entities.""" changes = {} for key, new_value in new_entity.items(): @@ -537,7 +536,7 @@ def _try_parse_url(url: str) -> Optional[str]: def _try_connect_to_server( url: str, timeout: Optional[float], - verify: Optional["Union[str, bool]"], + verify: Optional[Union[str, bool]], cert: Optional[str], ) -> Optional[str]: if timeout is None: @@ -637,7 +636,7 @@ def get_user_by_token( url: str, token: str, timeout: Optional[float] = None, -) -> Optional[Dict[str, Any]]: +) -> Optional[dict[str, Any]]: """Get user information by url and token. Args: @@ -647,7 +646,7 @@ def get_user_by_token( 'get_default_timeout' is used if not specified. Returns: - Optional[Dict[str, Any]]: User information if url and token are valid. + Optional[dict[str, Any]]: User information if url and token are valid. """ if timeout is None: @@ -699,7 +698,7 @@ def is_token_valid( def validate_url( url: str, timeout: Optional[int] = None, - verify: Optional["Union[str, bool]"] = None, + verify: Optional[Union[str, bool]] = None, cert: Optional[str] = None, ) -> str: """Validate url if is valid and server is available. @@ -1146,7 +1145,7 @@ def get_media_mime_type_for_content(content: bytes) -> Optional[str]: return _get_svg_mime_type(content) -def get_media_mime_type_for_stream(stream: "StreamType") -> Optional[str]: +def get_media_mime_type_for_stream(stream: StreamType) -> Optional[str]: # Read only 12 bytes to determine mime type content = stream.read(12) if len(content) < 12: @@ -1178,7 +1177,7 @@ def get_media_mime_type(filepath: str) -> Optional[str]: def take_web_action_event( server_url: str, action_token: str -) -> Dict[str, Any]: +) -> dict[str, Any]: """Take web action event using action token. Action token is generated by AYON server and passed to AYON launcher. @@ -1188,7 +1187,7 @@ def take_web_action_event( action_token (str): Action token. Returns: - Dict[str, Any]: Web action event. + dict[str, Any]: Web action event. """ response = requests.get( From 73cb911ab2da28693c96c8ecd64378ea56ce4e52 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 11:59:14 +0200 Subject: [PATCH 180/506] unified type hints in server api --- ayon_api/server_api.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f016c0b1f..e6764fed5 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -15,7 +15,7 @@ import uuid from contextlib import contextmanager import typing -from typing import Optional, Iterable, Generator, Any +from typing import Optional, Iterable, Generator, Any, Union import requests @@ -87,7 +87,6 @@ ) if typing.TYPE_CHECKING: - from typing import Union from .typing import ( ServerVersion, AnyEntityDict, @@ -273,7 +272,7 @@ def __init__( default_settings_variant: Optional[str] = None, sender_type: Optional[str] = None, sender: Optional[str] = None, - ssl_verify: Optional["Union[bool, str]"]=None, + ssl_verify: Optional[Union[bool, str]]=None, cert: Optional[str] = None, create_session: bool = True, timeout: Optional[float] = None, @@ -685,7 +684,7 @@ def set_default_service_username(self, username: Optional[str] = None): @contextmanager def as_username( self, - username: "Union[str, None]", + username: Optional[str], ignore_service_error: bool = False, ): """Service API will temporarily work as other user. @@ -693,7 +692,7 @@ def as_username( This method can be used only if service API key is logged in. Args: - username (Union[str, None]): Username to work as when service. + username (Optional[str]): Username to work as when service. ignore_service_error (Optional[bool]): Ignore error when service API key is not used. @@ -873,7 +872,7 @@ def get_server_version(self) -> str: self._server_version = self.get_info()["version"] return self._server_version - def get_server_version_tuple(self) -> "ServerVersion": + def get_server_version_tuple(self) -> ServerVersion: """Get server version as tuple. Version should match semantic version (https://semver.org/). @@ -897,7 +896,7 @@ def get_server_version_tuple(self) -> "ServerVersion": return self._server_version_tuple server_version = property(get_server_version) - server_version_tuple: "ServerVersion" = property( + server_version_tuple: ServerVersion = property( get_server_version_tuple ) @@ -1360,7 +1359,7 @@ def _download_file_to_stream( def download_file_to_stream( self, endpoint: str, - stream: "StreamType", + stream: StreamType, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> TransferProgress: @@ -1462,7 +1461,7 @@ def download_file( @staticmethod def _upload_chunks_iter( - file_stream: "StreamType", + file_stream: StreamType, progress: TransferProgress, chunk_size: int, ) -> Generator[bytes, None, None]: @@ -1494,7 +1493,7 @@ def _upload_chunks_iter( def _upload_file( self, url: str, - stream: "StreamType", + stream: StreamType, progress: TransferProgress, request_type: Optional[RequestType] = None, chunk_size: Optional[int] = None, @@ -1545,7 +1544,7 @@ def _upload_file( def upload_file_from_stream( self, endpoint: str, - stream: "StreamType", + stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, **kwargs @@ -1842,7 +1841,7 @@ def get_rest_entity_by_id( project_name: str, entity_type: str, entity_id: str, - ) -> Optional["AnyEntityDict"]: + ) -> Optional[AnyEntityDict]: """Get entity using REST on a project by its id. Args: @@ -2029,7 +2028,7 @@ def _prepare_fields( ) } - def _convert_entity_data(self, entity: "AnyEntityDict"): + def _convert_entity_data(self, entity: AnyEntityDict): if not entity or "data" not in entity: return From 3e6259ee55f5e15488887949cfe9f59b58d9319b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:20:29 +0200 Subject: [PATCH 181/506] add spaces between variable and default value --- 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 e6764fed5..6b18756a6 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -272,7 +272,7 @@ def __init__( default_settings_variant: Optional[str] = None, sender_type: Optional[str] = None, sender: Optional[str] = None, - ssl_verify: Optional[Union[bool, str]]=None, + ssl_verify: Optional[Union[bool, str]] = None, cert: Optional[str] = None, create_session: bool = True, timeout: Optional[float] = None, From c110ff91643ee24bac9f02db818e6b038e4dcae4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 12:20:42 +0200 Subject: [PATCH 182/506] added type hints to as user stact --- ayon_api/server_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6b18756a6..aaa588973 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -162,23 +162,23 @@ def clear(self): self._default_user = None @property - def username(self): + def username(self) -> Optional[str]: # Use '_user_ids' for boolean check to have ability "unset" # default user if self._user_ids: return self._last_user return self._default_user - def get_default_username(self): + def get_default_username(self) -> Optional[str]: return self._default_user - def set_default_username(self, username=None): + def set_default_username(self, username: Optional[str] = None) -> None: self._default_user = username default_username = property(get_default_username, set_default_username) @contextmanager - def as_user(self, username): + def as_user(self, username: Optional[str]) -> Generator[None, None, None]: self._last_user = username user_id = uuid.uuid4().hex self._user_ids.append(user_id) From ad79690a03f82999b65a30664cc8352fdd736d0f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Sep 2025 17:22:55 +0200 Subject: [PATCH 183/506] fix return type hints --- ayon_api/operations.py | 6 +++--- ayon_api/typing.py | 29 +++++++++++++++-------------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 6780ccfd9..dbdc99985 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -99,7 +99,7 @@ def new_folder_entity( created if not passed. Returns: - dict[str, Any]: Skeleton of folder entity. + NewFolderDict: Skeleton of folder entity. """ if attribs is None: @@ -119,7 +119,7 @@ def new_folder_entity( "parentId": parent_id, "data": data, "attrib": attribs, - "thumbnailId": thumbnail_id + "thumbnailId": thumbnail_id, } if status: output["status"] = status @@ -366,7 +366,7 @@ def new_workfile_info( if not passed. Returns: - dict[str, Any]: Skeleton of workfile info entity. + NewWorkfileDict: Skeleton of workfile info entity. """ if attribs is None: diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 8957cf92b..cce3f196a 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -8,6 +8,7 @@ Union, Optional, BinaryIO, + NotRequired, ) @@ -351,8 +352,8 @@ class NewFolderDict(TypedDict): data: dict[str, Any] attrib: dict[str, Any] thumbnailId: Optional[str] - status: Optional[str] - tags: Optional[list[str]] + status: NotRequired[str] + tags: NotRequired[list[str]] class NewProductDict(TypedDict): @@ -362,8 +363,8 @@ class NewProductDict(TypedDict): folderId: str data: dict[str, Any] attrib: dict[str, Any] - status: Optional[str] - tags: Optional[list[str]] + status: NotRequired[str] + tags: NotRequired[list[str]] class NewVersionDict(TypedDict): @@ -372,11 +373,11 @@ class NewVersionDict(TypedDict): productId: str attrib: dict[str, Any] data: dict[str, Any] - taskId: Optional[str] - thumbnailId: Optional[str] - author: Optional[str] - status: Optional[str] - tags: Optional[list[str]] + taskId: NotRequired[str] + thumbnailId: NotRequired[str] + author: NotRequired[str] + status: NotRequired[str] + tags: NotRequired[list[str]] class NewRepresentationDict(TypedDict): @@ -386,9 +387,9 @@ class NewRepresentationDict(TypedDict): data: dict[str, Any] attrib: dict[str, Any] files: list[dict[str, str]] - traits: Optional[dict[str, Any]] - status: Optional[str] - tags: Optional[list[str]] + traits: NotRequired[dict[str, Any]] + status: NotRequired[str] + tags: NotRequired[list[str]] class NewWorkfileDict(TypedDict): @@ -397,8 +398,8 @@ class NewWorkfileDict(TypedDict): path: str data: dict[str, Any] attrib: dict[str, Any] - status: Optional[str] - tags: Optional[list[str]] + status: NotRequired[str] + tags: NotRequired[list[str]] EventStatus = Literal[ From 5d17a4002ebf9d4616a827e8e4d9381bf69486d8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:37:36 +0200 Subject: [PATCH 184/506] removed some _CustomNone typehint --- ayon_api/entity_hub.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 5bdcc7d62..6f5421422 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -468,7 +468,7 @@ def add_new_product( self, name: str, product_type: str, - folder_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, + folder_id: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, @@ -481,7 +481,7 @@ def add_new_product( Args: name (str): Name of entity. product_type (str): Type of product. - folder_id (Optional[Union[str, _CustomNone]]): Parent folder id. + folder_id (Optional[str]): Parent folder id. tags (Optional[Iterable[str]]): Folder tags. attribs (dict[str, Any]): Attribute values. data (dict[str, Any]): Entity data (custom data). @@ -513,8 +513,8 @@ def add_new_product( def add_new_version( self, version: int, - product_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, - task_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, + product_id: Optional[str] = UNKNOWN_VALUE, + task_id: Optional[str] = UNKNOWN_VALUE, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, @@ -528,8 +528,8 @@ def add_new_version( Args: version (int): Version. - product_id (Union[Union[str, _CustomNone]]): Parent product id. - task_id (Union[Union[str, _CustomNone]]): Parent task id. + product_id (Optional[str]): Parent product id. + task_id (Optional[str]): Parent task id. status (Optional[str]): Task status. tags (Optional[Iterable[str]]): Folder tags. attribs (dict[str, Any]): Attribute values. @@ -1452,7 +1452,7 @@ class BaseEntity(ABC): def __init__( self, entity_id: Optional[str] = None, - parent_id: Optional[Union[str, _CustomNone]] = UNKNOWN_VALUE, + parent_id: Optional[str] = UNKNOWN_VALUE, attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, From c03e965f42a9d97b1ef960ca26e4a5e224aa5e81 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 09:51:25 +0200 Subject: [PATCH 185/506] fix type hint --- ayon_api/entity_hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 6f5421422..d63c6446e 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1259,7 +1259,7 @@ def __init__( def __contains__(self, key: str) -> bool: return key in self._attributes - def __getitem__(self, key: str) -> AttributeValue: + def __getitem__(self, key: str) -> AttributeValueType: return self._attributes[key].value def __setitem__(self, key: str, value: AttributeValueType) -> None: From e68a282a8d4132371dd7aa6967524a2be1d8ceb3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 10:47:52 +0200 Subject: [PATCH 186/506] type hint fixes --- ayon_api/entity_hub.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index d63c6446e..a424b6cb7 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -13,7 +13,7 @@ from .utils import create_entity_id, convert_entity_id, slugify_string if typing.TYPE_CHECKING: - from typing import Literal, Union, TypedDict + from typing import Literal, Union, TypedDict, NotRequired from .typing import ( AttributeSchemaDict, @@ -41,7 +41,7 @@ class ProjectStatusDict(TypedDict): state: Optional[StatusState] icon: Optional[str] color: Optional[str] - scope: Optional[StatusEntityType] + scope: NotRequired[Optional[StatusEntityType]] class _CustomNone: @@ -837,7 +837,7 @@ def reset_immutable_for_hierarchy_cache( if bottom_to_top: while reset_queue: entity_id: str = reset_queue.popleft() - entity: Optional["BaseEntity"] = self.get_entity_by_id( + entity: Optional[BaseEntity] = self.get_entity_by_id( entity_id ) if entity is None: @@ -847,7 +847,7 @@ def reset_immutable_for_hierarchy_cache( else: while reset_queue: entity_id: str = reset_queue.popleft() - entity: Optional["BaseEntity"] = self.get_entity_by_id( + entity: Optional[BaseEntity] = self.get_entity_by_id( entity_id ) if entity is None: @@ -1605,7 +1605,7 @@ def entity_type(self) -> EntityType: @property @abstractmethod - def parent_entity_types(self) -> list[str]: + def parent_entity_types(self) -> list[EntityType]: """Entity type corresponding to server. Returns: @@ -2530,11 +2530,11 @@ def move_after(self, other: ProjectStatus) -> None: self._validate_other_p_statuses(other) self._project_statuses.set_status_index(self, other.index + 1) - def to_data(self) -> dict[str, Any]: + def to_data(self) -> ProjectStatusDict: """Convert status to data. Returns: - dict[str, Any]: Status data. + ProjectStatusDict: Status data. """ output = { @@ -2993,7 +2993,7 @@ def set_name(self, name: str) -> None: return raise ValueError("It is not allowed to change project name.") - def get_parent(self, *args, **kwargs) -> None: + def get_parent(self, allow_fetch: bool = True) -> None: return None def set_parent(self, parent: Any) -> None: @@ -3047,7 +3047,9 @@ def set_statuses(self, statuses: list[ProjectStatusDict]) -> None: task_types = property(get_task_types, set_task_types) statuses = property(get_statuses, set_statuses) - def get_status_by_slugified_name(self, name: str) -> str: + def get_status_by_slugified_name( + self, name: str + ) -> Optional[ProjectStatus]: """Find status by name. Args: From 22c513ea936c097d131b9def49f2949a1099ad71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:19:15 +0200 Subject: [PATCH 187/506] implemented delete and update workfile --- ayon_api/__init__.py | 4 ++ ayon_api/_api.py | 69 +++++++++++++++++++++++++ ayon_api/_api_helpers/workfiles.py | 83 +++++++++++++++++++++++++++++- 3 files changed, 154 insertions(+), 2 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index a51f12bc4..0d077afcb 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -223,6 +223,8 @@ get_workfiles_info, get_workfile_info, get_workfile_info_by_id, + delete_workfile_info, + update_workfile_info, get_full_link_type_name, get_link_types, get_link_type, @@ -488,6 +490,8 @@ "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", + "delete_workfile_info", + "update_workfile_info", "get_full_link_type_name", "get_link_types", "get_link_type", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 500955995..5938ab65a 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6197,6 +6197,75 @@ def get_workfile_info_by_id( ) +def delete_workfile_info( + project_name: str, + workfile_id: str, +) -> None: + """Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + con = get_server_api_connection() + return con.delete_workfile_info( + project_name=project_name, + workfile_id=workfile_id, + ) + + +def update_workfile_info( + project_name: str, + workfile_id: str, + path: Optional[str] = None, + task_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, +) -> None: + """Update workfile entity on server. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. + + """ + con = get_server_api_connection() + return con.update_workfile_info( + project_name=project_name, + workfile_id=workfile_id, + path=path, + task_id=task_id, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + created_by=created_by, + updated_by=updated_by, + ) + + def get_full_link_type_name( link_type_name: str, input_type: str, diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index 0ef9083f5..8b2efb90f 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -2,10 +2,10 @@ import warnings import typing -from typing import Optional, Iterable, Generator +from typing import Optional, Iterable, Generator, Any from ayon_api.graphql_queries import workfiles_info_graphql_query - +from ayon_api.utils import NOT_SET from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: @@ -184,3 +184,82 @@ def get_workfile_info_by_id( ): return workfile_info return None + + def delete_workfile_info( + self, + project_name: str, + workfile_id: str, + ) -> None: + """Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + response = self.delete( + f"projects/{project_name}/workfiles/{workfile_id}" + ) + response.raise_for_status() + + def update_workfile_info( + self, + project_name: str, + workfile_id: str, + path: Optional[str] = None, + task_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, + ) -> None: + """Update workfile entity on server. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. + + """ + update_data = {} + for key, value in ( + ("path", path), + ("taskId", task_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("createdBy", created_by), + ("updatedBy", updated_by), + ): + if value is not None: + update_data[key] = value + + for key, value in ( + ("thumbnailId", thumbnail_id), + ): + if value is not NOT_SET: + update_data[key] = value + + response = self.patch( + f"projects/{project_name}/workfiles/{workfile_id}", + **update_data + ) + response.raise_for_status() From fb4572c440a3eb5c21b16d85958c83a427af4c27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:20:07 +0200 Subject: [PATCH 188/506] added option to unset user from dev bundle --- ayon_api/_api.py | 2 +- ayon_api/_api_helpers/bundles_addons.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 500955995..6ebff2aea 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -2235,7 +2235,7 @@ def update_bundle( is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, + dev_active_user: Optional[str] = _PLACEHOLDER, dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, ) -> None: """Update bundle on server. diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index 1b70967ec..85e998df6 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -10,7 +10,7 @@ TransferProgress, ) -from .base import BaseServerAPI +from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: from ayon_api.typing import ( @@ -135,7 +135,7 @@ def update_bundle( is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = None, + dev_active_user: Optional[str] = _PLACEHOLDER, dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, ) -> None: """Update bundle on server. @@ -171,11 +171,12 @@ def update_bundle( ("isProduction", is_production), ("isStaging", is_staging), ("isDev", is_dev), - ("activeUser", dev_active_user), ("addonDevelopment", dev_addons_config), ) if value is not None } + if dev_active_user is not _PLACEHOLDER: + body["activeUser"] = dev_active_user response = self.patch( f"bundles/{bundle_name}", From 840db482a2dcaabc111a19096a76ba49d3b80dda Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:24:45 +0200 Subject: [PATCH 189/506] use 'NOT_SET' instead of '_PLACEHOLDER' --- ayon_api/_api.py | 2 +- ayon_api/_api_helpers/bundles_addons.py | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 6ebff2aea..04b402aa8 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -2235,7 +2235,7 @@ def update_bundle( is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = _PLACEHOLDER, + dev_active_user: Optional[str] = NOT_SET, dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, ) -> None: """Update bundle on server. diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index 85e998df6..6355b62eb 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -8,9 +8,10 @@ RequestTypes, prepare_query_string, TransferProgress, + NOT_SET, ) -from .base import BaseServerAPI, _PLACEHOLDER +from .base import BaseServerAPI if typing.TYPE_CHECKING: from ayon_api.typing import ( @@ -135,7 +136,7 @@ def update_bundle( is_production: Optional[bool] = None, is_staging: Optional[bool] = None, is_dev: Optional[bool] = None, - dev_active_user: Optional[str] = _PLACEHOLDER, + dev_active_user: Optional[str] = NOT_SET, dev_addons_config: Optional[dict[str, DevBundleAddonInfoDict]] = None, ) -> None: """Update bundle on server. @@ -175,7 +176,7 @@ def update_bundle( ) if value is not None } - if dev_active_user is not _PLACEHOLDER: + if dev_active_user is not NOT_SET: body["activeUser"] = dev_active_user response = self.patch( From 92e0d07c3d8ca94a045a38780169055d604bf748 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:33:34 +0200 Subject: [PATCH 190/506] change list[str] to Iterable[str] --- ayon_api/entity_hub.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index a424b6cb7..3950176ba 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -343,7 +343,7 @@ def add_new_folder( label: Optional[str] = None, path: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, - tags: Optional[list[str]] = None, + tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, @@ -362,7 +362,7 @@ def add_new_folder( path (Optional[str]): Folder path. Path consist of all parent names with slash('/') used as separator. status (Optional[str]): Folder status. - tags (Optional[list[str]]): Folder tags. + tags (Optional[Iterable[str]]): Folder tags. attribs (dict[str, Any]): Attribute values. data (dict[str, Any]): Entity data (custom data). thumbnail_id (Optional[str]): Id of entity's thumbnail. @@ -1462,7 +1462,7 @@ def __init__( name=None, label=None, status: Optional[str] = UNKNOWN_VALUE, - tags: Optional[list[str]] = None, + tags: Optional[Iterable[str]] = None, thumbnail_id: Optional[str] = UNKNOWN_VALUE, ): if entity_hub is None: @@ -3145,7 +3145,7 @@ def __init__( label: Optional[str] = None, path: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, - tags: Optional[list[str]] = None, + tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, From 915fc97ddd9c055c2ffe33ffab6d9c614b311818 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:33:49 +0200 Subject: [PATCH 191/506] 'get_or_fetch_entity_by_id' returns union --- ayon_api/entity_hub.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 3950176ba..26d54615b 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -237,7 +237,14 @@ def get_or_fetch_entity_by_id( self, entity_id: str, entity_types: list[EntityType], - ) -> Optional[BaseEntity]: + ) -> Union[ + ProjectEntity, + FolderEntity, + TaskEntity, + ProductEntity, + VersionEntity, + None + ]: """Get or query entity based on it's id and possible entity types. This is a helper function when entity id is known but entity type may @@ -314,7 +321,14 @@ def get_or_query_entity_by_id( self, entity_id: str, entity_types: list[EntityType], - ) -> Optional[BaseEntity]: + ) -> Union[ + ProjectEntity, + FolderEntity, + TaskEntity, + ProductEntity, + VersionEntity, + None + ]: """Get or query entity based on it's id and possible entity types.""" warnings.warn( "Method 'get_or_query_entity_by_id' is deprecated. " From cc351f58cf26f2d597c06298752fca1fdf4cf97b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:37:31 +0200 Subject: [PATCH 192/506] do explicit none comparison --- ayon_api/entity_hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 26d54615b..2d8d7b799 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -84,7 +84,7 @@ def __init__( project_name: str, connection: Optional[ServerAPI] = None ) -> None: - if not connection: + if connection is None: connection = get_server_api_connection() self._connection = connection From 7f5aa53e9b2b6d9721e6d232f214546df5bcc2a6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:37:38 +0200 Subject: [PATCH 193/506] keep same type hint --- ayon_api/entity_hub.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 2d8d7b799..ef86825f7 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -3446,7 +3446,7 @@ def set_assignees(self, assignees: Iterable[str]) -> None: assignees = property(get_assignees, set_assignees) - def add_child(self, child: BaseEntity) -> None: + def add_child(self, child: Union[BaseEntity, str]) -> None: raise ValueError("Task does not support to add children") @property From ae1413f1e5ce642c92a486814228fa453d85a66b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 15 Sep 2025 16:47:20 +0200 Subject: [PATCH 194/506] fix parenting type hints --- ayon_api/entity_hub.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index ef86825f7..aee0a34af 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -3410,10 +3410,10 @@ def lock(self) -> None: self._orig_task_type = self._task_type self._orig_assignees = copy.deepcopy(self._assignees) - def get_folder_id(self) -> Union[str, _CustomNone]: + def get_folder_id(self) -> Union[str, None, _CustomNone]: return self._parent_id - def set_folder_id(self, folder_id): + def set_folder_id(self, folder_id: str) -> None: self.set_parent_id(folder_id) folder_id = property(get_folder_id, set_folder_id) @@ -3554,7 +3554,7 @@ def __init__( self._orig_product_type = product_type - def get_folder_id(self) -> Union[str, _CustomNone]: + def get_folder_id(self) -> Union[str, None, _CustomNone]: return self._parent_id def set_folder_id(self, folder_id: str) -> None: @@ -3677,7 +3677,7 @@ def set_version(self, version: int) -> None: version = property(get_version, set_version) - def get_product_id(self) -> Optional[str]: + def get_product_id(self) -> Union[str, None, _CustomNone]: return self._parent_id def set_product_id(self, product_id: str) -> None: @@ -3685,7 +3685,7 @@ def set_product_id(self, product_id: str) -> None: product_id = property(get_product_id, set_product_id) - def get_task_id(self) -> Optional[str]: + def get_task_id(self) -> Union[str, None, _CustomNone]: return self._task_id def set_task_id(self, task_id: Optional[str]) -> None: From 08842fb3cc35c46454862e614ce07749a94d06fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:54:01 +0200 Subject: [PATCH 195/506] change default value of attribs is None --- ayon_api/entity_hub.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index aee0a34af..d8be69cee 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -358,7 +358,7 @@ def add_new_folder( path: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, @@ -377,7 +377,7 @@ def add_new_folder( with slash('/') used as separator. status (Optional[str]): Folder status. tags (Optional[Iterable[str]]): Folder tags. - attribs (dict[str, Any]): Attribute values. + attribs (Optional[dict[str, Any]]): Attribute values. data (dict[str, Any]): Entity data (custom data). thumbnail_id (Optional[str]): Id of entity's thumbnail. active (Optional[bool]): Is entity active. @@ -417,7 +417,7 @@ def add_new_task( label: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, assignees: Optional[Iterable[str]] = None, thumbnail_id: Optional[str] = UNKNOWN_VALUE, @@ -436,7 +436,7 @@ def add_new_task( label (Optional[str]): Task label. status (Optional[str]): Task status. tags (Optional[Iterable[str]]): Folder tags. - attribs (dict[str, Any]): Attribute values. + attribs (Optional[dict[str, Any]]): Attribute values. data (dict[str, Any]): Entity data (custom data). assignees (Optional[Iterable[str]]): User assignees to the task. thumbnail_id (Optional[str]): Id of entity's thumbnail. @@ -484,7 +484,7 @@ def add_new_product( product_type: str, folder_id: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, @@ -497,7 +497,7 @@ def add_new_product( product_type (str): Type of product. folder_id (Optional[str]): Parent folder id. tags (Optional[Iterable[str]]): Folder tags. - attribs (dict[str, Any]): Attribute values. + attribs (Optional[dict[str, Any]]): Attribute values. data (dict[str, Any]): Entity data (custom data). active (bool): Is entity active. entity_id (Optional[str]): Id of the entity. New id is created if @@ -531,7 +531,7 @@ def add_new_version( task_id: Optional[str] = UNKNOWN_VALUE, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, @@ -546,7 +546,7 @@ def add_new_version( task_id (Optional[str]): Parent task id. status (Optional[str]): Task status. tags (Optional[Iterable[str]]): Folder tags. - attribs (dict[str, Any]): Attribute values. + attribs (Optional[dict[str, Any]]): Attribute values. data (dict[str, Any]): Entity data (custom data). thumbnail_id (Optional[str]): Id of entity's thumbnail. active (bool): Is entity active. @@ -1261,7 +1261,7 @@ class Attributes: def __init__( self, attrib_keys: Iterable[str], - values: Optional[dict[str, Any]] = UNKNOWN_VALUE, + values: Optional[dict[str, Any]] = None, ) -> None: if values in (UNKNOWN_VALUE, None): values = {} @@ -1467,7 +1467,7 @@ def __init__( self, entity_id: Optional[str] = None, parent_id: Optional[str] = UNKNOWN_VALUE, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, created: Optional[bool] = None, @@ -2966,7 +2966,7 @@ def __init__( folder_types: list[dict[str, Any]], task_types: list[dict[str, Any]], statuses: list[ProjectStatusDict], - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_hub: EntityHub = None, @@ -3160,7 +3160,7 @@ def __init__( path: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, @@ -3369,7 +3369,7 @@ def __init__( label: Optional[str] = None, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, assignees: Optional[Iterable[str]] = None, thumbnail_id: Optional[str] = UNKNOWN_VALUE, @@ -3532,7 +3532,7 @@ def __init__( product_type: str, folder_id: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, entity_id: Optional[str] = None, @@ -3643,7 +3643,7 @@ def __init__( task_id: Optional[str] = UNKNOWN_VALUE, status: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - attribs: Optional[dict[str, Any]] = UNKNOWN_VALUE, + attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = UNKNOWN_VALUE, thumbnail_id: Optional[str] = UNKNOWN_VALUE, active: Optional[bool] = UNKNOWN_VALUE, From 2da80706b2d74e97cbcb1cf85acda38d489be43d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:00:45 +0200 Subject: [PATCH 196/506] remove duplicated methods --- ayon_api/entity_hub.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index d8be69cee..e5fbcbc79 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1940,26 +1940,6 @@ def remove_child(self, child: Union[BaseEntity, str]) -> None: self._children_ids.discard(child_id) self._entity_hub.unset_entity_parent(child_id, self.id) - def get_thumbnail_id(self) -> str: - """Thumbnail id of entity. - - Returns: - Optional[str]: Thumbnail id or none if is not set. - - """ - return self._thumbnail_id - - def set_thumbnail_id(self, thumbnail_id: Optional[str]) -> None: - """Change thumbnail id. - - Args: - thumbnail_id (Optional[str]): Thumbnail id for entity. - - """ - self._thumbnail_id = thumbnail_id - - thumbnail_id = property(get_thumbnail_id, set_thumbnail_id) - @property def created(self) -> bool: """Entity is new. From c91b71c818f03f6b674ed0315505f5d2f5bb4baa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:49:27 +0200 Subject: [PATCH 197/506] use full type hints --- ayon_api/entity_hub.py | 96 ++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index e5fbcbc79..f111b6b0f 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -6,7 +6,7 @@ import warnings from abc import ABC, abstractmethod import typing -from typing import Optional, Iterable, Any, Generator, Type +from typing import Optional, Iterable, Any, Generator from .server_api import ServerAPI from ._api import get_server_api_connection @@ -815,7 +815,7 @@ def _fetch_entity_children(self, entity: BaseEntity) -> None: def get_entity_children( self, entity: BaseEntity, allow_fetch: bool = True - ) -> Union[list[BaseEntity], Type[UNKNOWN_VALUE]]: + ) -> Union[list[BaseEntity], _CustomNone]: children_ids = entity.get_children_ids(allow_fetch=False) if children_ids is not UNKNOWN_VALUE: return entity.get_children() @@ -1466,19 +1466,19 @@ class BaseEntity(ABC): def __init__( self, entity_id: Optional[str] = None, - parent_id: Optional[str] = UNKNOWN_VALUE, + parent_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, attribs: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = UNKNOWN_VALUE, - active: Optional[bool] = UNKNOWN_VALUE, + data: Union[dict[str, Any], None, _CustomNone] = UNKNOWN_VALUE, + active: Union[bool, _CustomNone] = UNKNOWN_VALUE, created: Optional[bool] = None, entity_hub: EntityHub = None, # Optional arguments - name=None, - label=None, - status: Optional[str] = UNKNOWN_VALUE, + name: Optional[str] = None, + label: Optional[str] = None, + status: Union[str, _CustomNone] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, - thumbnail_id: Optional[str] = UNKNOWN_VALUE, - ): + thumbnail_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, + ) -> None: if entity_hub is None: raise ValueError("Missing required kwarg 'entity_hub'") @@ -1539,7 +1539,7 @@ def __init__( self._immutable_for_hierarchy_cache = None - def __repr__(self): + def __repr__(self) -> str: return f"<{self.__class__.__name__} - {self.id}>" def __getitem__(self, item: str) -> Any: @@ -1795,11 +1795,11 @@ def lock(self) -> None: def _get_entity_by_id(self, entity_id: str) -> Optional[BaseEntity]: return self._entity_hub.get_entity_by_id(entity_id) - def get_parent_id(self) -> Optional[str]: + def get_parent_id(self) -> Union[str, None, _CustomNone]: """Parent entity id. Returns: - Optional[str]: Parent entity id or none if is not set. + Union[str, None, _CustomNone]: Parent entity id. """ return self._parent_id @@ -1824,11 +1824,13 @@ def set_parent_id(self, parent_id: Optional[str]) -> None: parent_id = property(get_parent_id, set_parent_id) - def get_parent(self, allow_fetch: bool = True) -> Optional[BaseEntity]: + def get_parent( + self, allow_fetch: bool = True + ) -> Union[BaseEntity, None, _CustomNone]: """Parent entity. Returns: - Optional[BaseEntity]: Parent object. + Union[BaseEntity, None, _CustomNone]: Parent object. """ parent = self._entity_hub.get_entity_by_id(self._parent_id) @@ -1845,11 +1847,11 @@ def get_parent(self, allow_fetch: bool = True) -> Optional[BaseEntity]: self._parent_id, self.parent_entity_types ) - def set_parent(self, parent: BaseEntity) -> None: + def set_parent(self, parent: Optional[BaseEntity]) -> None: """Change parent object. Args: - parent (BaseEntity): New parent for entity. + parent (Optional[BaseEntity]): New parent for entity. Raises: TypeError: If validation of parent does not pass. @@ -1862,7 +1864,9 @@ def set_parent(self, parent: BaseEntity) -> None: parent = property(get_parent, set_parent) - def get_children_ids(self, allow_fetch=True): + def get_children_ids( + self, allow_fetch: bool = True + ) -> Union[set[str], _CustomNone]: """Access to children objects. Todos: @@ -1872,7 +1876,7 @@ def get_children_ids(self, allow_fetch=True): hierarchy. Returns: - Union[list[str], Type[UNKNOWN_VALUE]]: Children iterator. + Union[list[str], _CustomNone]: Children iterator. """ if self._children_ids is UNKNOWN_VALUE: @@ -1885,11 +1889,11 @@ def get_children_ids(self, allow_fetch=True): def get_children( self, allow_fetch: bool = True - ) -> list[Union[BaseEntity, Type[UNKNOWN_VALUE]]]: + ) -> Union[list[BaseEntity], _CustomNone]: """Access to children objects. Returns: - Union[list[BaseEntity], Type[UNKNOWN_VALUE]]: Children iterator. + Union[list[BaseEntity], _CustomNone]: Children iterator. """ if self._children_ids is UNKNOWN_VALUE: @@ -1959,7 +1963,7 @@ def fill_children_ids(self, children_ids: Iterable[str]) -> None: """ self._children_ids = set(children_ids) - def get_name(self) -> str: + def get_name(self) -> Optional[str]: if not self._supports_name: raise NotImplementedError( f"Name is not supported for '{self.entity_type}'." @@ -2006,11 +2010,11 @@ def _get_label_value(self) -> Optional[str]: label = property(get_label, set_label) - def get_thumbnail_id(self) -> Optional[str]: + def get_thumbnail_id(self) -> Union[str, None, _CustomNone]: """Thumbnail id of entity. Returns: - Optional[str]: Thumbnail id or none if is not set. + Optional[str]: Thumbnail id or None if is not set. """ if not self._supports_thumbnail: @@ -2038,7 +2042,7 @@ def get_status(self) -> Union[str, _CustomNone]: """Folder status. Returns: - Union[str, UNKNOWN_VALUE]: Folder status or 'UNKNOWN_VALUE'. + Union[str, UNKNOWN_VALUE]: Entity status or 'UNKNOWN_VALUE'. """ if not self._supports_status: @@ -2947,8 +2951,8 @@ def __init__( task_types: list[dict[str, Any]], statuses: list[ProjectStatusDict], attribs: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = UNKNOWN_VALUE, - active: Optional[bool] = UNKNOWN_VALUE, + data: Union[dict[str, Any], None, _CustomNone] = UNKNOWN_VALUE, + active: Union[bool, _CustomNone] = UNKNOWN_VALUE, entity_hub: EntityHub = None, ): super().__init__( @@ -3135,15 +3139,15 @@ def __init__( self, name: str, folder_type: str, - parent_id: Optional[str] = UNKNOWN_VALUE, + parent_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, label: Optional[str] = None, path: Optional[str] = None, - status: Optional[str] = UNKNOWN_VALUE, + status: Union[str, _CustomNone] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = UNKNOWN_VALUE, - thumbnail_id: Optional[str] = UNKNOWN_VALUE, - active: Optional[bool] = UNKNOWN_VALUE, + data: Union[dict[str, Any], _CustomNone] = UNKNOWN_VALUE, + thumbnail_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, + active: Union[bool, _CustomNone] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, @@ -3345,15 +3349,15 @@ def __init__( self, name: str, task_type: str, - folder_id: Optional[str] = UNKNOWN_VALUE, + folder_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, label: Optional[str] = None, - status: Optional[str] = UNKNOWN_VALUE, + status: Union[str, _CustomNone] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = UNKNOWN_VALUE, + data: Union[dict[str, Any], None, _CustomNone] = UNKNOWN_VALUE, assignees: Optional[Iterable[str]] = None, - thumbnail_id: Optional[str] = UNKNOWN_VALUE, - active: Optional[bool] = UNKNOWN_VALUE, + thumbnail_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, + active: Union[bool, _CustomNone] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, @@ -3510,11 +3514,11 @@ def __init__( self, name: str, product_type: str, - folder_id: Optional[str] = UNKNOWN_VALUE, + folder_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = UNKNOWN_VALUE, - active: Optional[bool] = UNKNOWN_VALUE, + data: Union[dict[str, Any], None, _CustomNone] = UNKNOWN_VALUE, + active: Union[bool, _CustomNone] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, @@ -3619,14 +3623,14 @@ class VersionEntity(BaseEntity): def __init__( self, version: int, - product_id: Optional[str] = UNKNOWN_VALUE, - task_id: Optional[str] = UNKNOWN_VALUE, - status: Optional[str] = UNKNOWN_VALUE, + product_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, + task_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, + status: Union[str, _CustomNone] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = None, - data: Optional[dict[str, Any]] = UNKNOWN_VALUE, - thumbnail_id: Optional[str] = UNKNOWN_VALUE, - active: Optional[bool] = UNKNOWN_VALUE, + data: Union[dict[str, Any], None, _CustomNone] = UNKNOWN_VALUE, + thumbnail_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, + active: Union[bool, _CustomNone] = UNKNOWN_VALUE, entity_id: Optional[str] = None, created: Optional[bool] = None, entity_hub: EntityHub = None, From d915ae81ec0c26b6f7b631bea52398c403a041b0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 16:49:50 +0200 Subject: [PATCH 198/506] comment doubled set up --- ayon_api/entity_hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index f111b6b0f..a8a72c0fd 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1789,8 +1789,8 @@ def lock(self) -> None: self._orig_status = self._status if self._supports_tags: self._orig_tags = copy.deepcopy(self._tags) - if self._supports_thumbnail: - self._orig_thumbnail_id = self._thumbnail_id + # if self._supports_thumbnail: + # self._orig_thumbnail_id = self._thumbnail_id def _get_entity_by_id(self, entity_id: str) -> Optional[BaseEntity]: return self._entity_hub.get_entity_by_id(entity_id) From a4a2d59bfdbc6ffc98f6c449b4f02927b69e1da6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 16 Sep 2025 17:39:58 +0200 Subject: [PATCH 199/506] add comment to the file --- ayon_api/entity_hub.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index a8a72c0fd..62e6a4f54 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1,3 +1,15 @@ +"""Entity hub is a helper for AYON project entities. + +It provides a way to create new entities and to manage existing ones. + +Note @iLLiCiTiT this really needs cleanup. + +- Remove optional arguments and attributes from 'BaseEntity'. +- More reasonable order of attributes and require positional arguments in + some cases. +- Make clear why UNKNOWN_VALUE is used in some default values for arguments. + +""" from __future__ import annotations import re From 67c45575427a5db478d32cdef9f4b55dbbd67e5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:14:00 +0200 Subject: [PATCH 200/506] remove functions that are not needed --- ayon_api/__init__.py | 2 -- ayon_api/_api.py | 24 ------------------------ ayon_api/graphql_queries.py | 24 ------------------------ 3 files changed, 50 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 14b8be7c8..6d439a11a 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -192,7 +192,6 @@ get_project_product_types, get_product_type_names, get_product_base_types, - get_project_product_base_types, get_product_base_type_names, create_product, update_product, @@ -462,7 +461,6 @@ "get_project_product_types", "get_product_type_names", "get_product_base_types", - "get_project_product_base_types", "get_product_base_type_names", "create_product", "update_product", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 02972040f..efec547ae 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4997,30 +4997,6 @@ def get_project_product_types( ) -def get_project_product_base_types( - project_name: str, - fields: Optional[Iterable[str]] = None, -) -> List["ProductBaseTypeDict"]: - """Base types of products available in a project. - - Filter only product base types available in a project. - - Args: - project_name (str): Name of project where to look for - product base types. - fields (Optional[Iterable[str]]): Product base types fields to query. - - Returns: - List[ProductBaseTypeDict]: Product base types information. - - """ - con = get_server_api_connection() - return con.get_project_product_base_types( - project_name=project_name, - fields=fields, - ) - - def get_product_type_names( project_name: Optional[str] = None, product_ids: Optional[Iterable[str]] = None, diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 80f4c5c6a..ce769a18d 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -121,30 +121,6 @@ def product_types_query(fields): return query -def project_product_base_types_query(fields): - query = GraphQlQuery("ProjectProductBaseTypes") - project_query = query.add_field("project") - project_name_var = query.add_variable("projectName", "String!") - project_query.set_filter("name", project_name_var) - product_base_types_field = project_query.add_field("productBaseTypes") - nested_fields = fields_to_dict(fields) - - query_queue = collections.deque() - for key, value in nested_fields.items(): - query_queue.append((key, value, product_base_types_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 - - def folders_graphql_query(fields): query = GraphQlQuery("FoldersQuery") project_name_var = query.add_variable("projectName", "String!") From 68132009a22887093bf2b8721176e5915bf901c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:18:24 +0200 Subject: [PATCH 201/506] remove 'get_product_base_type_names' again --- ayon_api/__init__.py | 2 -- ayon_api/_api.py | 27 --------------------------- 2 files changed, 29 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 6d439a11a..9d4fe4834 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -192,7 +192,6 @@ get_project_product_types, get_product_type_names, get_product_base_types, - get_product_base_type_names, create_product, update_product, delete_product, @@ -461,7 +460,6 @@ "get_project_product_types", "get_product_type_names", "get_product_base_types", - "get_product_base_type_names", "create_product", "update_product", "delete_product", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index efec547ae..6ffc47ae6 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -5024,33 +5024,6 @@ def get_product_type_names( ) -def get_product_base_type_names( - project_name: Optional[str] = None, - product_ids: Optional[Iterable[str]] = None, -) -> Set[str]: - """Base product type names. - - Warnings: - Similar use case as `get_product_type_names` but for base - product types. - - Args: - project_name (Optional[str]): Name of project where to look for - queried entities. - product_ids (Optional[Iterable[str]]): Product ids filter. Can be - used only with 'project_name'. - - Returns: - set[str]: Base product type names. - - """ - con = get_server_api_connection() - return con.get_product_base_type_names( - project_name=project_name, - product_ids=product_ids, - ) - - def create_product( project_name: str, name: str, From 44d139a0ae9dd92f9226b4e7756cbc79c25fd6e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:46:31 +0200 Subject: [PATCH 202/506] remove 'get_product_base_types' from '_api' --- ayon_api/_api.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 6ffc47ae6..392b0b182 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4952,27 +4952,6 @@ def get_product_types( ) -def get_product_base_types( - fields: Optional[Iterable[str]] = None, -) -> List["ProductBaseTypeDict"]: - """Base types of products. - - This is the server-wide information. Product base types have 'name', 'icon' - and 'color'. - - Args: - fields (Optional[Iterable[str]]): Product base types fields to query. - - Returns: - list[ProductBaseTypeDict]: Product base types information. - - """ - con = get_server_api_connection() - return con.get_product_base_types( - fields=fields, - ) - - def get_project_product_types( project_name: str, fields: Optional[Iterable[str]] = None, From 819a69fa2c121f7a0090b029646cc4875ada0160 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:46:48 +0200 Subject: [PATCH 203/506] remove icon and color from product base types fields --- ayon_api/constants.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 97b881099..f5f494048 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -84,9 +84,9 @@ # --- Product base type --- DEFAULT_PRODUCT_BASE_TYPE_FIELDS = { + # Ignore 'icon' and 'color' + # - current server implementation always returns 'null' "name", - "icon", - "color", } # --- Project --- From 26a3f8404bf5ab5ec867c0c849dce82adab8a02a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:53:19 +0200 Subject: [PATCH 204/506] remove unused 'ProductBaseTypeDict' --- ayon_api/_api.py | 1 - ayon_api/typing.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 392b0b182..73fe7c77b 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -68,7 +68,6 @@ FolderDict, TaskDict, ProductDict, - ProductBaseTypeDict, VersionDict, RepresentationDict, WorkfileInfoDict, diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 646d0f799..cce3f196a 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -573,9 +573,3 @@ class ActionConfigResponse(TypedDict): class EntityListAttributeDefinitionDict(TypedDict): name: str data: dict[str, Any] - - -class ProductBaseTypeDict(TypedDict): - name: str - color: Optional[str] - icon: Optional[str] From 937ada3b493c2a3bc323a431ede8176fec024009 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:53:38 +0200 Subject: [PATCH 205/506] remove 'productBaseType' fields handling --- ayon_api/server_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 07761ef79..5c97e02b8 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1821,9 +1821,6 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: elif entity_type == "productType": entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS) - elif entity_type == "productBaseType": - entity_type_defaults = set(DEFAULT_PRODUCT_BASE_TYPE_FIELDS) - elif entity_type == "workfile": entity_type_defaults = set(DEFAULT_WORKFILE_INFO_FIELDS) From 7d63720d63a8b5850ee2e86638724e9d5c909747 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:53:51 +0200 Subject: [PATCH 206/506] handle 'productBaseType' field if passed in as is --- ayon_api/_api_helpers/projects.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 09fe66d98..7eab3dbbf 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -6,7 +6,10 @@ import typing from typing import Optional, Generator, Iterable, Any -from ayon_api.constants import PROJECT_NAME_REGEX +from ayon_api.constants import ( + PROJECT_NAME_REGEX, + DEFAULT_PRODUCT_BASE_TYPE_FIELDS, +) from ayon_api.utils import prepare_query_string, fill_own_attribs from ayon_api.graphql_queries import projects_graphql_query @@ -595,6 +598,11 @@ def _get_project_graphql_fields( if fields is None: return set(), True + if "productBaseType" in fields: + fields.discard("productBaseType") + for pbt_field_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS: + fields.add(f"productBaseType.{pbt_field_name}") + has_product_types = False graphql_fields = set() for field in fields: From 0691d8e9224458a90b03abf1ebb90cdd54c6262f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 14:54:41 +0200 Subject: [PATCH 207/506] remove 'get_product_base_types' --- ayon_api/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 9d4fe4834..0d077afcb 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -191,7 +191,6 @@ get_product_types, get_project_product_types, get_product_type_names, - get_product_base_types, create_product, update_product, delete_product, @@ -459,7 +458,6 @@ "get_product_types", "get_project_product_types", "get_product_type_names", - "get_product_base_types", "create_product", "update_product", "delete_product", From 7d984af8de71331199c2ab380f166fdd335e424b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:07:54 +0200 Subject: [PATCH 208/506] added base of product base type support logic --- ayon_api/_api_helpers/base.py | 3 +++ ayon_api/entity_hub.py | 4 +++- ayon_api/server_api.py | 12 ++++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index f49b10a9b..c54898919 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -23,6 +23,9 @@ class BaseServerAPI: def log(self) -> logging.Logger: raise NotImplementedError() + def product_base_type_supported(self) -> bool: + raise NotImplementedError() + def get_server_version(self) -> str: raise NotImplementedError() diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 4de53f321..f0a80f602 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -130,6 +130,9 @@ def project_entity(self) -> ProjectEntity: self.fill_project_from_server() return self._project_entity + def product_base_type_supported(self) -> bool: + return self._connection.product_base_type_supported() + def get_attributes_for_type( self, entity_type: EntityType ) -> dict[str, AttributeSchemaDict]: @@ -3526,7 +3529,6 @@ def to_create_body_data(self) -> dict[str, Any]: class ProductEntity(BaseEntity): _supports_name = True _supports_tags = True - _supports_base_type = True entity_type = "product" parent_entity_types = ["folder"] diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 5c97e02b8..72238c646 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -327,6 +327,7 @@ def __init__( self._server_version_tuple = None self._graphql_allows_traits_in_representations: Optional[bool] = None + self._product_base_type_supported = None self._session = None @@ -911,6 +912,17 @@ def graphql_allows_traits_in_representations(self) -> bool: ) return self._graphql_allows_traits_in_representations + def product_base_type_supported(self) -> bool: + """Product base types are available on server.""" + if self._product_base_type_supported is None: + major, minor, patch, _, _ = self.server_version_tuple + self._product_base_type_supported = False + # TODO implement when server version of the support is known + # self._product_base_type_supported = ( + # (major, minor, patch) >= (1, 12, 0) + # ) + return self._product_base_type_supported + def _get_user_info(self) -> Optional[dict[str, Any]]: if self._access_token is None: return None From 818e7626242052865190842d21b8c68b6995f848 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:08:06 +0200 Subject: [PATCH 209/506] implement product base type handling in entity hub --- ayon_api/entity_hub.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index f0a80f602..adf7c671a 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -3562,6 +3562,7 @@ def __init__( self._product_base_type = product_base_type self._orig_product_type = product_type + self._orig_product_base_type = product_base_type def get_folder_id(self) -> Union[str, None, _CustomNone]: return self._parent_id @@ -3597,6 +3598,7 @@ def set_product_base_type(self, product_base_type: str) -> None: def lock(self) -> None: super().lock() self._orig_product_type = self._product_type + self._orig_product_base_type = self._product_base_type @property def changes(self) -> dict[str, Any]: @@ -3608,6 +3610,12 @@ def changes(self) -> dict[str, Any]: if self._orig_product_type != self._product_type: changes["productType"] = self._product_type + if ( + self._entity_hub.product_base_type_supported() + and self._orig_product_base_type != self._product_base_type + ): + changes["productBaseType"] = self._product_base_type + return changes @classmethod @@ -3638,7 +3646,10 @@ def to_create_body_data(self) -> dict[str, Any]: "folderId": self.parent_id, } - if self._supports_base_type: + if ( + self._entity_hub.product_base_type_supported() + and self.product_base_type + ): output["productBaseType"] = self.product_base_type attrib = self.attribs.to_dict() From a52c1f9dee8de795d09bfe0f1dabf91e2fd092fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:08:25 +0200 Subject: [PATCH 210/506] added product base type to products api methods --- ayon_api/_api_helpers/products.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index 13fdb9b80..369adb8db 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -5,6 +5,7 @@ import typing from typing import Optional, Iterable, Generator, Any +from ayon_api.exceptions import UnsupportedServerVersion from ayon_api.utils import ( prepare_list_filters, create_entity_id, @@ -378,6 +379,7 @@ def create_product( tags: Optional[Iterable[str]] =None, status: Optional[str] = None, active: Optional[bool] = None, + product_base_type: Optional[str] = None, product_id: Optional[str] = None, ) -> str: """Create new product. @@ -392,6 +394,7 @@ def create_product( tags (Optional[Iterable[str]]): Product tags. status (Optional[str]): Product status. active (Optional[bool]): Product active state. + product_base_type (Optional[str]): Product base type. product_id (Optional[str]): Product id. If not passed new id is generated. @@ -399,6 +402,14 @@ def create_product( str: Product id. """ + if ( + product_base_type is not None + and not self.product_base_type_supported() + ): + raise UnsupportedServerVersion( + "Product base type is not supported for your server version." + ) + if not product_id: product_id = create_entity_id() create_data = { @@ -408,6 +419,7 @@ def create_product( "folderId": folder_id, } for key, value in ( + ("productBaseType", product_base_type), ("attrib", attrib), ("data", data), ("tags", tags), @@ -431,6 +443,7 @@ def update_product( name: Optional[str] = None, folder_id: Optional[str] = None, product_type: Optional[str] = None, + product_base_type: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, @@ -450,6 +463,7 @@ def update_product( name (Optional[str]): New product name. folder_id (Optional[str]): New product id. product_type (Optional[str]): New product type. + product_base_type (Optional[str]): New product base type. attrib (Optional[dict[str, Any]]): New product attributes. data (Optional[dict[str, Any]]): New product data. tags (Optional[Iterable[str]]): New product tags. @@ -457,10 +471,19 @@ def update_product( active (Optional[bool]): New product active state. """ + if ( + product_base_type is not None + and not self.product_base_type_supported() + ): + raise UnsupportedServerVersion( + "Product base type is not supported for your server version." + ) + update_data = {} for key, value in ( ("name", name), ("productType", product_type), + ("productBaseType", product_base_type), ("folderId", folder_id), ("attrib", attrib), ("data", data), From 3dae224c288bc41fd812dcd635d3754034a66716 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:08:40 +0200 Subject: [PATCH 211/506] change order of product_base_type argument --- ayon_api/operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 34df630c4..dd478f687 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1146,8 +1146,8 @@ def create_product( tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, - product_id: Optional[str] = None, product_base_type: Optional[str] = None, + product_id: Optional[str] = None, ) -> CreateOperation: """Create new product. @@ -1160,9 +1160,9 @@ def create_product( tags (Optional[Iterable[str]]): Product tags. status (Optional[str]): Product status. active (Optional[bool]): Product active state. + product_base_type (Optional[str]): Product base type. product_id (Optional[str]): Product id. If not passed new id is generated. - product_base_type (Optional[str]): Product base type. Returns: CreateOperation: Object of create operation. From 82291c65b2eccff582728bff5eddf7fe2313b0dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:16:03 +0200 Subject: [PATCH 212/506] change one more argument order --- ayon_api/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index dd478f687..6c70f2ffd 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -136,8 +136,8 @@ def new_product_entity( tags: Optional[list[str]] = None, attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, - entity_id: Optional[str] = None, product_base_type: Optional[str] = None, + entity_id: Optional[str] = None, ) -> NewProductDict: """Create skeleton data of the product entity. From 7c83f11190cebd612c11243246087f09f95dc3a4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Sep 2025 15:22:45 +0200 Subject: [PATCH 213/506] remove unused import --- ayon_api/server_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 72238c646..659931a97 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -26,7 +26,6 @@ DEFAULT_PROJECT_STATUSES_FIELDS, DEFAULT_PROJECT_TAGS_FIELDS, DEFAULT_PRODUCT_TYPE_FIELDS, - DEFAULT_PRODUCT_BASE_TYPE_FIELDS, DEFAULT_PROJECT_FIELDS, DEFAULT_FOLDER_FIELDS, DEFAULT_TASK_FIELDS, From 072bef19dd36b756dd808c6631d151103f57e4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 22 Sep 2025 18:40:12 +0200 Subject: [PATCH 214/506] :recycle: add check and more support in graphql --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 22 +++++++++++++++++----- ayon_api/_api_helpers/products.py | 15 ++++++++++++--- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 0d077afcb..0e437e053 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -54,6 +54,7 @@ get_info, get_server_version, get_server_version_tuple, + product_base_type_supported, get_users, get_user_by_name, get_user, @@ -321,6 +322,7 @@ "get_info", "get_server_version", "get_server_version_tuple", + "product_base_type_supported", "get_users", "get_user_by_name", "get_user", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 73fe7c77b..e6809caac 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -719,6 +719,13 @@ def get_server_version_tuple() -> ServerVersion: return con.get_server_version_tuple() +def product_base_type_supported() -> bool: + """Product base types are available on server. + """ + con = get_server_api_connection() + return con.product_base_type_supported() + + def get_users( project_name: Optional[str] = None, usernames: Optional[Iterable[str]] = None, @@ -4809,22 +4816,21 @@ def get_products( fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, ) -> Generator[ProductDict, None, None]: - """Query products from the server. + """Query products from server. Todos: Separate 'name_by_folder_ids' filtering to separated method. It cannot be combined with some other filters. Args: - project_name (str): Name of the project. + project_name (str): Name of project. product_ids (Optional[Iterable[str]]): Task ids to filter. product_names (Optional[Iterable[str]]): Task names used for filtering. folder_ids (Optional[Iterable[str]]): Ids of task parents. - Use 'None' if folder is direct child of the project. + Use 'None' if folder is direct child of project. product_types (Optional[Iterable[str]]): Product types used for filtering. - product_base_types (Optional[Iterable[str]]): Product base types product_name_regex (Optional[str]): Filter products by name regex. product_path_regex (Optional[str]): Filter products by path regex. Path starts with folder path and ends with product name. @@ -4935,7 +4941,7 @@ def get_product_types( ) -> list[ProductTypeDict]: """Types of products. - This is the server-wide information. Product types have 'name', 'icon' and + This is server wide information. Product types have 'name', 'icon' and 'color'. Args: @@ -5012,6 +5018,7 @@ def create_product( tags: Optional[Iterable[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, + product_base_type: Optional[str] = None, product_id: Optional[str] = None, ) -> str: """Create new product. @@ -5026,6 +5033,7 @@ def create_product( tags (Optional[Iterable[str]]): Product tags. status (Optional[str]): Product status. active (Optional[bool]): Product active state. + product_base_type (Optional[str]): Product base type. product_id (Optional[str]): Product id. If not passed new id is generated. @@ -5044,6 +5052,7 @@ def create_product( tags=tags, status=status, active=active, + product_base_type=product_base_type, product_id=product_id, ) @@ -5054,6 +5063,7 @@ def update_product( name: Optional[str] = None, folder_id: Optional[str] = None, product_type: Optional[str] = None, + product_base_type: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, tags: Optional[Iterable[str]] = None, @@ -5073,6 +5083,7 @@ def update_product( name (Optional[str]): New product name. folder_id (Optional[str]): New product id. product_type (Optional[str]): New product type. + product_base_type (Optional[str]): New product base type. attrib (Optional[dict[str, Any]]): New product attributes. data (Optional[dict[str, Any]]): New product data. tags (Optional[Iterable[str]]): New product tags. @@ -5087,6 +5098,7 @@ def update_product( name=name, folder_id=folder_id, product_type=product_type, + product_base_type=product_base_type, attrib=attrib, data=data, tags=tags, diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index 369adb8db..75bc780ff 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -33,9 +33,10 @@ def get_products( self, project_name: str, product_ids: Optional[Iterable[str]] = None, - product_names: Optional[Iterable[str]]=None, - folder_ids: Optional[Iterable[str]]=None, - product_types: Optional[Iterable[str]]=None, + product_names: Optional[Iterable[str]] = None, + folder_ids: Optional[Iterable[str]] = None, + product_types: Optional[Iterable[str]] = None, + product_base_types: Optional[Iterable[str]] = None, product_name_regex: Optional[str] = None, product_path_regex: Optional[str] = None, names_by_folder_ids: Optional[dict[str, Iterable[str]]] = None, @@ -60,6 +61,8 @@ def get_products( Use 'None' if folder is direct child of project. product_types (Optional[Iterable[str]]): Product types used for filtering. + product_base_types (Optional[Iterable[str]]): Product base types + used for filtering. product_name_regex (Optional[str]): Filter products by name regex. product_path_regex (Optional[str]): Filter products by path regex. Path starts with folder path and ends with product name. @@ -84,6 +87,11 @@ def get_products( if not project_name: return + if product_base_types and not self.product_base_type_supported(): + raise UnsupportedServerVersion( + "Product base type is not supported for your server version." + ) + # Prepare these filters before 'name_by_filter_ids' filter filter_product_names = None if product_names is not None: @@ -151,6 +159,7 @@ def get_products( filters, ("productIds", product_ids), ("productTypes", product_types), + ("productBaseTypes", product_base_types), ("productStatuses", statuses), ("productTags", tags), ): From f1fd855d06d612dcb74a16a29de87a6cf85066b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:19:56 +0200 Subject: [PATCH 215/506] remove optional from typehints --- ayon_api/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index fa9b7ac21..baf271277 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -416,9 +416,9 @@ def entity_data_json_default(value: Any) -> Any: def slugify_string( input_string: str, - separator: Optional[str] = "_", - slug_whitelist: Optional[Iterable[str]] = SLUGIFY_WHITELIST, - split_chars: Optional[Iterable[str]] = SLUGIFY_SEP_WHITELIST, + separator: str = "_", + slug_whitelist: Iterable[str] = SLUGIFY_WHITELIST, + split_chars: Iterable[str] = SLUGIFY_SEP_WHITELIST, min_length: int = 1, lower: bool = False, make_set: bool = False, From 9cadc6b32cdbd273260d041b3d0fff6cf89b71dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:21:45 +0200 Subject: [PATCH 216/506] bump version to '1.2.0' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 46f78d0a5..4a4bec631 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.1.5-dev" +__version__ = "1.2.0" diff --git a/pyproject.toml b/pyproject.toml index f4b9b4098..14d919f29 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.1.5-dev" +version = "1.2.0" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.1.5-dev" +version = "1.2.0v" description = "AYON Python API" authors = [ "ynput.io " From 218a8aea5b34b64a2aec453730e25c2767f64c92 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:22:47 +0200 Subject: [PATCH 217/506] fix version in pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 14d919f29..d13324434 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.2.0v" +version = "1.2.0" description = "AYON Python API" authors = [ "ynput.io " From b1f8587be1d280a8bb79befc162e588f65539485 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 15:23:58 +0200 Subject: [PATCH 218/506] bump version to '1.2.1-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 4a4bec631..900624445 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.0" +__version__ = "1.2.1-dev" diff --git a/pyproject.toml b/pyproject.toml index d13324434..189e6aaa9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.2.0" +version = "1.2.1-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.2.0" +version = "1.2.1-dev" description = "AYON Python API" authors = [ "ynput.io " From d521e80a226734c5a99fa62614e4ec385b544224 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:58:11 +0200 Subject: [PATCH 219/506] fix build of the package --- pyproject.toml | 3 ++- setup.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 189e6aaa9..57f6abf1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -34,7 +34,8 @@ authors = [ "ynput.io " ] packages = [ - { include = "ayon_api" } + { include = "ayon_api" }, + { include = "ayon_api/_api_helpers/*.py" }, ] [tool.poetry.dependencies] diff --git a/setup.py b/setup.py index 87f7a462b..060a8673c 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ name="ayon_api", version=_version_content["__version__"], py_modules=["ayon_api"], - packages=["ayon_api"], + packages=["ayon_api", "ayon_api._api_helpers"], author="ynput.io", author_email="info@ynput.io", license="Apache License (2.0)", From 2306ec8975b32fb63eedfd8ccc6aae4ac7388692 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 16:58:45 +0200 Subject: [PATCH 220/506] bump version to 1.2.1 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 900624445..98dfba833 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.1-dev" +__version__ = "1.2.1" diff --git a/pyproject.toml b/pyproject.toml index 57f6abf1f..5d53cda58 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.2.1-dev" +version = "1.2.1" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.2.1-dev" +version = "1.2.1" description = "AYON Python API" authors = [ "ynput.io " From e483d064bd9c7277b03389c782f101f3b025c3bf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:00:15 +0200 Subject: [PATCH 221/506] bump version to '1.2.2-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 98dfba833..552e1099e 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.1" +__version__ = "1.2.2-dev" diff --git a/pyproject.toml b/pyproject.toml index 5d53cda58..a519e2b69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon-python-api" -version = "1.2.1" +version = "1.2.2-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon-python-api" -version = "1.2.1" +version = "1.2.2-dev" description = "AYON Python API" authors = [ "ynput.io " From c38bc2e605516830f9ce38e7cfb3b335500bb12f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 25 Sep 2025 17:58:06 +0200 Subject: [PATCH 222/506] change project name to 'ayon_python_api' --- pyproject.toml | 4 ++-- setup.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a519e2b69..e2a6eb937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "ayon-python-api" +name = "ayon_python_api" version = "1.2.2-dev" description = "AYON Python API" license = {file = "LICENSE"} @@ -27,7 +27,7 @@ requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.poetry] -name = "ayon-python-api" +name = "ayon_python_api" version = "1.2.2-dev" description = "AYON Python API" authors = [ diff --git a/setup.py b/setup.py index 060a8673c..402a48546 100644 --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ exec(open(VERSION_PATH).read(), _version_content) setup( - name="ayon_api", + name="ayon_python_api", version=_version_content["__version__"], py_modules=["ayon_api"], packages=["ayon_api", "ayon_api._api_helpers"], From 8a29300644e36a679bc4f84cbc8baa347f32bf2e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:05:34 +0200 Subject: [PATCH 223/506] added create workfile info method --- ayon_api/_api_helpers/workfiles.py | 62 +++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index 8b2efb90f..b083d5577 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -5,7 +5,8 @@ from typing import Optional, Iterable, Generator, Any from ayon_api.graphql_queries import workfiles_info_graphql_query -from ayon_api.utils import NOT_SET +from ayon_api.utils import NOT_SET, create_entity_id + from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: @@ -185,6 +186,65 @@ def get_workfile_info_by_id( return workfile_info return None + def create_workfile_info( + self, + project_name: str, + path: str, + task_id: str, + *, + thumbnail_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + workfile_id: Optional[str] = None, + ) -> str: + """Create new workfile. + + Args: + project_name (str): Project name. + path (str): Representation name. + task_id (str): Parent task id. + thumbnail_id (Optional[str]): Thumbnail id. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + workfile_id (Optional[str]): Workfile info id. If not + passed new id is generated. + + Returns: + str: Workfile info id. + + """ + if workfile_id is None: + workfile_id = create_entity_id() + + create_data = { + "id": workfile_id, + "path": path, + "taskId": task_id, + } + for key, value in ( + ("thumbnailId", thumbnail_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + create_data[key] = value + + response = self.post( + f"projects/{project_name}/workfiles", + **create_data + ) + response.raise_for_status() + return workfile_id + def delete_workfile_info( self, project_name: str, From 3d15f4183b53c3cc19312397aacbf8bae49336d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:09:23 +0200 Subject: [PATCH 224/506] add support for older server versions --- ayon_api/_api_helpers/base.py | 5 +++++ ayon_api/_api_helpers/workfiles.py | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index f49b10a9b..3e7499506 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -109,6 +109,11 @@ def get_project( ) -> Optional[ProjectDict]: raise NotImplementedError() + def get_user( + self, username: Optional[str] = None + ) -> Optional[dict[str, Any]]: + raise NotImplementedError() + def _prepare_fields( self, entity_type: str, diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index b083d5577..c955befe4 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -238,6 +238,13 @@ def create_workfile_info( if value is not None: create_data[key] = value + major, minor, patch, _, _ = self.get_server_version_tuple() + if (major, minor, patch) < (1, 1, 3): + user = self.get_user() + username = user["name"] + create_data["createdBy"] = username + create_data["updatedBy"] = username + response = self.post( f"projects/{project_name}/workfiles", **create_data From 3389f745621939ca5f3730521217abbf275d5088 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 8 Oct 2025 16:09:53 +0200 Subject: [PATCH 225/506] added workfiles methods to operations --- ayon_api/operations.py | 140 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index dbdc99985..5a514162e 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1544,3 +1544,143 @@ def delete_representation( return self.delete_entity( project_name, "representation", representation_id ) + + def create_workfile_info( + self, + project_name: str, + path: str, + task_id: str, + *, + thumbnail_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + workfile_id: Optional[str] = None, + ) -> CreateOperation: + """Create new workfile. + + Args: + project_name (str): Project name. + path (str): Representation name. + task_id (str): Parent task id. + thumbnail_id (Optional[str]): Thumbnail id. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + workfile_id (Optional[str]): Workfile info id. If not + passed new id is generated. + + Returns: + CreateOperation: Object of create operation. + + """ + if workfile_id is None: + workfile_id = create_entity_id() + + create_data = { + "id": workfile_id, + "path": path, + "taskId": task_id, + } + for key, value in ( + ("thumbnailId", thumbnail_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ): + if value is not None: + create_data[key] = value + + return self.create_entity( + project_name, + "workfile", + create_data + ) + + def update_workfile_info( + self, + project_name: str, + workfile_id: str, + path: Optional[str] = None, + task_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, + ) -> UpdateOperation: + """Update workfile info entity on server. + + Update of ``data`` will override existing value on folder entity. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. + + Returns: + UpdateOperation: Object of update operation. + + """ + update_data = {} + for key, value in ( + ("path", path), + ("taskId", task_id), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("active", active), + ("thumbnailId", thumbnail_id), + ("createdBy", created_by), + ("updatedBy", updated_by), + ): + if value is not None: + update_data[key] = value + + return self.update_entity( + project_name, + "workfile", + workfile_id, + update_data + ) + + def delete_workfile( + self, + project_name: str, + workfile_id: str, + ) -> DeleteOperation: + """Delete representation. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile info id to delete. + + Returns: + DeleteOperation: Object of delete operation. + + """ + return self.delete_entity( + project_name, "workfile", workfile_id + ) From dc40920d6609958222a3923578c793486467e9fa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 11:37:48 +0200 Subject: [PATCH 226/506] add public function --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 47 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 0d077afcb..6aff269aa 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -223,6 +223,7 @@ get_workfiles_info, get_workfile_info, get_workfile_info_by_id, + create_workfile_info, delete_workfile_info, update_workfile_info, get_full_link_type_name, @@ -490,6 +491,7 @@ "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", + "create_workfile_info", "delete_workfile_info", "update_workfile_info", "get_full_link_type_name", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 14d1988b7..72ff5aa9d 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6197,6 +6197,53 @@ def get_workfile_info_by_id( ) +def create_workfile_info( + project_name: str, + path: str, + task_id: str, + *, + thumbnail_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + workfile_id: Optional[str] = None, +) -> str: + """Create new workfile. + + Args: + project_name (str): Project name. + path (str): Representation name. + task_id (str): Parent task id. + thumbnail_id (Optional[str]): Thumbnail id. + attrib (Optional[dict[str, Any]]): Representation attributes. + data (Optional[dict[str, Any]]): Representation data. + tags (Optional[Iterable[str]]): Representation tags. + status (Optional[str]): Representation status. + active (Optional[bool]): Representation active state. + workfile_id (Optional[str]): Workfile info id. If not + passed new id is generated. + + Returns: + str: Workfile info id. + + """ + con = get_server_api_connection() + return con.create_workfile_info( + project_name=project_name, + path=path, + task_id=task_id, + thumbnail_id=thumbnail_id, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + workfile_id=workfile_id, + ) + + def delete_workfile_info( project_name: str, workfile_id: str, From ffbf0a15714c680c28531ea4a7f918559b0eeaa2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:55:29 +0200 Subject: [PATCH 227/506] add support for background operations --- ayon_api/__init__.py | 4 + ayon_api/_api.py | 74 +++++++++++++++++++ ayon_api/server_api.py | 162 ++++++++++++++++++++++++++++++++++------- ayon_api/typing.py | 6 ++ 4 files changed, 219 insertions(+), 27 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 0d077afcb..a41fee778 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -80,6 +80,8 @@ get_default_fields_for_type, get_rest_entity_by_id, send_batch_operations, + send_background_batch_operations, + get_background_operations_status, get_installers, create_installer, update_installer, @@ -347,6 +349,8 @@ "get_default_fields_for_type", "get_rest_entity_by_id", "send_batch_operations", + "send_background_batch_operations", + "get_background_operations_status", "get_installers", "create_installer", "update_installer", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 14d1988b7..7e7a23917 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1253,6 +1253,80 @@ def send_batch_operations( ) +def send_background_batch_operations( + project_name: str, + operations: list[dict[str, Any]], + *, + can_fail: bool = False, + wait: bool = False, + raise_on_fail: bool = True, +) -> BackgroundOperation: + """Post multiple CRUD 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. + + Compared to 'send_batch_operations' this function creates a task on + server which then can be periodically checked for a status and + receive it's result. + + When used with 'wait' set to 'True' this method blocks until task is + finished. Which makes it work as 'send_batch_operations' + but safer for large operations batch as is not bound to + response timeout. + + 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. + wait (bool): Wait for operations to end. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. Used when 'wait' is enabled. + + 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: + BackgroundOperation: Background operation. + + """ + con = get_server_api_connection() + return con.send_background_batch_operations( + project_name=project_name, + operations=operations, + can_fail=can_fail, + wait=wait, + raise_on_fail=raise_on_fail, + ) + + +def get_background_operations_status( + project_name: str, + task_id: str, +) -> BackgroundOperation: + """Get status of background operations task. + + Args: + project_name (str): Project name. + task_id (str): Backgorund operation task id. + + Returns: + BackgroundOperation: Background operation. + + """ + con = get_server_api_connection() + return con.get_background_operations_status( + project_name=project_name, + task_id=task_id, + ) + + def get_installers( version: Optional[str] = None, platform_name: Optional[str] = None, diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index aaa588973..f933971d8 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -91,6 +91,7 @@ ServerVersion, AnyEntityDict, StreamType, + BackgroundOperation, ) VERSION_REGEX = re.compile( @@ -1870,7 +1871,7 @@ def send_batch_operations( project_name: str, operations: list[dict[str, Any]], can_fail: bool = False, - raise_on_fail: bool = True + raise_on_fail: bool = True, ) -> list[dict[str, Any]]: """Post multiple CRUD operations to server. @@ -1904,17 +1905,98 @@ def send_batch_operations( raise_on_fail, ) - def _send_batch_operations( + def send_background_batch_operations( self, - uri: str, + project_name: str, operations: list[dict[str, Any]], - can_fail: bool, - raise_on_fail: bool - ) -> list[dict[str, Any]]: - if not operations: - return [] + *, + can_fail: bool = False, + wait: bool = False, + raise_on_fail: bool = True, + ) -> BackgroundOperation: + """Post multiple CRUD 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. + + Compared to 'send_batch_operations' this function creates a task on + server which then can be periodically checked for a status and + receive it's result. + + When used with 'wait' set to 'True' this method blocks until task is + finished. Which makes it work as 'send_batch_operations' + but safer for large operations batch as is not bound to + response timeout. + + 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. + wait (bool): Wait for operations to end. + raise_on_fail (Optional[bool]): Raise exception if an operation + fails. You can handle failed operations on your own + when set to 'False'. Used when 'wait' is enabled. + + 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: + BackgroundOperation: Background operation. + + """ + operations_body = self._prepare_operations_body(operations) + response = self.post( + f"projects/{project_name}/operations/background", + operations=operations_body, + canFail=can_fail + ) + response.raise_for_status() + if not wait: + return response.data - body_by_id = {} + task_id = response["id"] + time.sleep(0.1) + while True: + op_status = self.get_background_operations_status( + project_name, task_id + ) + if op_status["status"] == "completed": + break + time.sleep(1) + + if raise_on_fail: + self._validate_operations_result( + op_status["result"], operations_body + ) + return op_status + + def get_background_operations_status( + self, project_name: str, task_id: str + ) -> BackgroundOperation: + """Get status of background operations task. + + Args: + project_name (str): Project name. + task_id (str): Backgorund operation task id. + + Returns: + BackgroundOperation: Background operation. + + """ + response = self.get( + f"projects/{project_name}/operations/background/{task_id}" + ) + response.raise_for_status() + return response.data + + def _prepare_operations_body( + self, operations: list[dict[str, Any]] + ) -> list[dict[str, Any]]: operations_body = [] for operation in operations: if not operation: @@ -1936,42 +2018,68 @@ def _send_batch_operations( ) )) - body_by_id[op_id] = body operations_body.append(body) + return operations_body + def _send_batch_operations( + self, + uri: str, + operations: list[dict[str, Any]], + can_fail: bool, + raise_on_fail: bool + ) -> list[dict[str, Any]]: + if not operations: + return [] + + operations_body = self._prepare_operations_body(operations) if not operations_body: return [] - result = self.post( + response = self.post( uri, operations=operations_body, canFail=can_fail ) - op_results = result.get("operations") + op_results = response.get("operations") if op_results is None: - detail = result.get("detail") + detail = response.get("detail") if detail: raise FailedOperations(f"Operation failed. Detail: {detail}") raise FailedOperations( - f"Operation failed. Content: {result.text}" + f"Operation failed. Content: {response.text}" ) - if result.get("success") or not raise_on_fail: - return op_results - - for op_result in op_results: - if not op_result["success"]: - operation_id = op_result["id"] - raise FailedOperations(( - "Operation \"{}\" failed with data:\n{}\nDetail: {}." - ).format( - operation_id, - json.dumps(body_by_id[operation_id], indent=4), - op_result["detail"], - )) + if raise_on_fail: + self._validate_operations_result(response.data, operations_body) return op_results + def _validate_operations_result( + self, + result: dict[str, Any], + operations_body: list[dict[str, Any]], + ) -> None: + if result.get("success"): + return None + + print(result) + for op_result in result["operations"]: + if op_result["success"]: + continue + + operation_id = op_result["id"] + operation = next( + op + for op in operations_body + if op["id"] == operation_id + ) + detail = op_result["detail"] + raise FailedOperations( + f"Operation \"{operation_id}\" failed with data:" + f"\n{json.dumps(operation, indent=4)}" + f"\nDetail: {detail}." + ) + def _prepare_fields( self, entity_type: str, fields: set[str], own_attributes: bool = False ): diff --git a/ayon_api/typing.py b/ayon_api/typing.py index cce3f196a..f7d9c8232 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -89,6 +89,12 @@ class EventFilter(TypedDict): operator: Literal["and", "or"] +class BackgroundOperation(TypedDict): + id: str + status: Literal["pending", "in_progress", "completed"] + result: Optional[dict[str, Any]] + + AttributeScope = Literal[ "project", "folder", From a4abdc5a9c912fc071bf3fb810c89b751f7e2994 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:57:26 +0200 Subject: [PATCH 228/506] use background operations in entity hub --- ayon_api/entity_hub.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 62e6a4f54..5e52ef8f1 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1225,8 +1225,11 @@ def commit_changes(self) -> None: if not entity.created: operations_body.append(self._get_delete_body(entity)) - self._connection.send_batch_operations( - self.project_name, operations_body + self._connection.send_background_batch_operations( + self.project_name, + operations_body, + can_fail=False, + wait=True, ) if post_project_changes: self._connection.update_project( From 6ad86644c6f1836a76c29a21740f020a4f63e735 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:03:49 +0200 Subject: [PATCH 229/506] add missing import --- ayon_api/_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 7e7a23917..7c21795b9 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -48,6 +48,7 @@ ActivityReferenceType, EntityListEntityType, EntityListItemMode, + BackgroundOperation, LinkDirection, EventFilter, EventStatus, From ef1b3d0cec3951c01093f3bccf3b614f6c387c3c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:17:40 +0200 Subject: [PATCH 230/506] rename 'BackgroundOperation' to 'BackgroundOperationTask' --- ayon_api/_api.py | 10 +++++----- ayon_api/server_api.py | 10 +++++----- ayon_api/typing.py | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 7c21795b9..b6a101f1c 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -48,7 +48,7 @@ ActivityReferenceType, EntityListEntityType, EntityListItemMode, - BackgroundOperation, + BackgroundOperationTask, LinkDirection, EventFilter, EventStatus, @@ -1261,7 +1261,7 @@ def send_background_batch_operations( can_fail: bool = False, wait: bool = False, raise_on_fail: bool = True, -) -> BackgroundOperation: +) -> BackgroundOperationTask: """Post multiple CRUD operations to server. When multiple changes should be made on server side this is the best @@ -1294,7 +1294,7 @@ def send_background_batch_operations( or 'raise_on_fail' is enabled and any operation fails. Returns: - BackgroundOperation: Background operation. + BackgroundOperationTask: Background operation. """ con = get_server_api_connection() @@ -1310,7 +1310,7 @@ def send_background_batch_operations( def get_background_operations_status( project_name: str, task_id: str, -) -> BackgroundOperation: +) -> BackgroundOperationTask: """Get status of background operations task. Args: @@ -1318,7 +1318,7 @@ def get_background_operations_status( task_id (str): Backgorund operation task id. Returns: - BackgroundOperation: Background operation. + BackgroundOperationTask: Background operation. """ con = get_server_api_connection() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f933971d8..87bb044d5 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -91,7 +91,7 @@ ServerVersion, AnyEntityDict, StreamType, - BackgroundOperation, + BackgroundOperationTask, ) VERSION_REGEX = re.compile( @@ -1913,7 +1913,7 @@ def send_background_batch_operations( can_fail: bool = False, wait: bool = False, raise_on_fail: bool = True, - ) -> BackgroundOperation: + ) -> BackgroundOperationTask: """Post multiple CRUD operations to server. When multiple changes should be made on server side this is the best @@ -1946,7 +1946,7 @@ def send_background_batch_operations( or 'raise_on_fail' is enabled and any operation fails. Returns: - BackgroundOperation: Background operation. + BackgroundOperationTask: Background operation. """ operations_body = self._prepare_operations_body(operations) @@ -1977,7 +1977,7 @@ def send_background_batch_operations( def get_background_operations_status( self, project_name: str, task_id: str - ) -> BackgroundOperation: + ) -> BackgroundOperationTask: """Get status of background operations task. Args: @@ -1985,7 +1985,7 @@ def get_background_operations_status( task_id (str): Backgorund operation task id. Returns: - BackgroundOperation: Background operation. + BackgroundOperationTask: Background operation. """ response = self.get( diff --git a/ayon_api/typing.py b/ayon_api/typing.py index f7d9c8232..24458cebb 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -89,7 +89,7 @@ class EventFilter(TypedDict): operator: Literal["and", "or"] -class BackgroundOperation(TypedDict): +class BackgroundOperationTask(TypedDict): id: str status: Literal["pending", "in_progress", "completed"] result: Optional[dict[str, Any]] From f9c1421bf4d0eaf56b70cf00d6b23dcc6f1285d5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:21:44 +0200 Subject: [PATCH 231/506] rename function to 'create_workfile_entity' --- ayon_api/__init__.py | 4 ++-- ayon_api/_api.py | 4 ++-- ayon_api/_api_helpers/workfiles.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 8694155fd..5c6029307 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -225,7 +225,7 @@ get_workfiles_info, get_workfile_info, get_workfile_info_by_id, - create_workfile_info, + create_workfile_entity, delete_workfile_info, update_workfile_info, get_full_link_type_name, @@ -495,7 +495,7 @@ "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", - "create_workfile_info", + "create_workfile_entity", "delete_workfile_info", "update_workfile_info", "get_full_link_type_name", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 4f7f10bfc..07fca87c0 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6272,7 +6272,7 @@ def get_workfile_info_by_id( ) -def create_workfile_info( +def create_workfile_entity( project_name: str, path: str, task_id: str, @@ -6305,7 +6305,7 @@ def create_workfile_info( """ con = get_server_api_connection() - return con.create_workfile_info( + return con.create_workfile_entity( project_name=project_name, path=path, task_id=task_id, diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index c955befe4..53614b3ef 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -186,7 +186,7 @@ def get_workfile_info_by_id( return workfile_info return None - def create_workfile_info( + def create_workfile_entity( self, project_name: str, path: str, From 3004221d8a4ccf1f9aab4b5b1169b312e9e1a0dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 16:42:32 +0200 Subject: [PATCH 232/506] rename workfile info to entity --- ayon_api/__init__.py | 18 +- ayon_api/_api.py | 218 ++++++++++++++++++++--- ayon_api/_api_helpers/workfiles.py | 277 ++++++++++++++++++++++++----- 3 files changed, 444 insertions(+), 69 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 5c6029307..7cf0825f1 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -222,12 +222,17 @@ create_representation, update_representation, delete_representation, + get_workfile_entities, + get_workfile_entity, + get_workfile_entity_by_id, + create_workfile_entity, + update_workfile_entity, + delete_workfile_entity, get_workfiles_info, get_workfile_info, get_workfile_info_by_id, - create_workfile_entity, - delete_workfile_info, update_workfile_info, + delete_workfile_info, get_full_link_type_name, get_link_types, get_link_type, @@ -492,12 +497,17 @@ "create_representation", "update_representation", "delete_representation", + "get_workfile_entities", + "get_workfile_entity", + "get_workfile_entity_by_id", + "create_workfile_entity", + "update_workfile_entity", + "delete_workfile_entity", "get_workfiles_info", "get_workfile_info", "get_workfile_info_by_id", - "create_workfile_entity", - "delete_workfile_info", "update_workfile_info", + "delete_workfile_info", "get_full_link_type_name", "get_link_types", "get_link_type", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 07fca87c0..0d93e03e0 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6157,8 +6157,9 @@ def delete_representation( ) -def get_workfiles_info( +def get_workfile_entities( project_name: str, + *, workfile_ids: Optional[Iterable[str]] = None, task_ids: Optional[Iterable[str]] = None, paths: Optional[Iterable[str]] = None, @@ -6167,7 +6168,6 @@ def get_workfiles_info( tags: Optional[Iterable[str]] = None, has_links: Optional[str] = None, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, ) -> Generator[WorkfileInfoDict, None, None]: """Workfile info entities by passed filters. @@ -6186,8 +6186,6 @@ def get_workfiles_info( fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. Returns: Generator[WorkfileInfoDict, None, None]: Queried workfile info @@ -6195,7 +6193,7 @@ def get_workfiles_info( """ con = get_server_api_connection() - return con.get_workfiles_info( + return con.get_workfile_entities( project_name=project_name, workfile_ids=workfile_ids, task_ids=task_ids, @@ -6205,16 +6203,15 @@ def get_workfiles_info( tags=tags, has_links=has_links, fields=fields, - own_attributes=own_attributes, ) -def get_workfile_info( +def get_workfile_entity( project_name: str, task_id: str, path: str, + *, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, ) -> Optional[WorkfileInfoDict]: """Workfile info entity by task id and workfile path. @@ -6225,28 +6222,25 @@ def get_workfile_info( fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. Returns: Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_workfile_info( + return con.get_workfile_entity( project_name=project_name, task_id=task_id, path=path, fields=fields, - own_attributes=own_attributes, ) -def get_workfile_info_by_id( +def get_workfile_entity_by_id( project_name: str, workfile_id: str, + *, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, ) -> Optional[WorkfileInfoDict]: """Workfile info entity by id. @@ -6256,19 +6250,16 @@ def get_workfile_info_by_id( fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. Returns: Optional[WorkfileInfoDict]: Workfile info entity or None. """ con = get_server_api_connection() - return con.get_workfile_info_by_id( + return con.get_workfile_entity_by_id( project_name=project_name, workfile_id=workfile_id, fields=fields, - own_attributes=own_attributes, ) @@ -6319,7 +6310,59 @@ def create_workfile_entity( ) -def delete_workfile_info( +def update_workfile_entity( + project_name: str, + workfile_id: str, + *, + path: Optional[str] = None, + task_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, +) -> None: + """Update workfile entity on server. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. + + """ + con = get_server_api_connection() + return con.update_workfile_entity( + project_name=project_name, + workfile_id=workfile_id, + path=path, + task_id=task_id, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + created_by=created_by, + updated_by=updated_by, + ) + + +def delete_workfile_entity( project_name: str, workfile_id: str, ) -> None: @@ -6331,12 +6374,127 @@ def delete_workfile_info( """ con = get_server_api_connection() - return con.delete_workfile_info( + return con.delete_workfile_entity( project_name=project_name, workfile_id=workfile_id, ) +def get_workfiles_info( + project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] = None, + paths: Optional[Iterable[str]] = None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str] = None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Generator[WorkfileInfoDict, None, None]: + """DEPRECATED Workfile info entities by passed filters. + + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. + + """ + con = get_server_api_connection() + return con.get_workfiles_info( + project_name=project_name, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, + statuses=statuses, + tags=tags, + has_links=has_links, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_workfile_info( + project_name: str, + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[WorkfileInfoDict]: + """DEPRECATED Workfile info entity by task id and workfile path. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + con = get_server_api_connection() + return con.get_workfile_info( + project_name=project_name, + task_id=task_id, + path=path, + fields=fields, + own_attributes=own_attributes, + ) + + +def get_workfile_info_by_id( + project_name: str, + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, +) -> Optional[WorkfileInfoDict]: + """DEPRECATED Workfile info entity by id. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + con = get_server_api_connection() + return con.get_workfile_info_by_id( + project_name=project_name, + workfile_id=workfile_id, + fields=fields, + own_attributes=own_attributes, + ) + + def update_workfile_info( project_name: str, workfile_id: str, @@ -6351,7 +6509,7 @@ def update_workfile_info( created_by: Optional[str] = None, updated_by: Optional[str] = None, ) -> None: - """Update workfile entity on server. + """DEPRECATED Update workfile entity on server. Update of ``attrib`` does change only passed attributes. If you want to unset value, use ``None``. @@ -6388,6 +6546,24 @@ def update_workfile_info( ) +def delete_workfile_info( + project_name: str, + workfile_id: str, +) -> None: + """DEPRECATED Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + con = get_server_api_connection() + return con.delete_workfile_info( + project_name=project_name, + workfile_id=workfile_id, + ) + + def get_full_link_type_name( link_type_name: str, input_type: str, diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index 53614b3ef..a0e35b9a8 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -14,9 +14,10 @@ class WorkfilesAPI(BaseServerAPI): - def get_workfiles_info( + def get_workfile_entities( self, project_name: str, + *, workfile_ids: Optional[Iterable[str]] = None, task_ids: Optional[Iterable[str]] =None, paths: Optional[Iterable[str]] =None, @@ -25,7 +26,6 @@ def get_workfiles_info( tags: Optional[Iterable[str]] = None, has_links: Optional[str]=None, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, ) -> Generator[WorkfileInfoDict, None, None]: """Workfile info entities by passed filters. @@ -44,8 +44,6 @@ def get_workfiles_info( fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. Returns: Generator[WorkfileInfoDict, None, None]: Queried workfile info @@ -95,16 +93,6 @@ def get_workfiles_info( fields = set(fields) self._prepare_fields("workfile", fields) - if own_attributes is not _PLACEHOLDER: - warnings.warn( - ( - "'own_attributes' is not supported for workfiles. The" - " argument will be removed form function signature in" - " future (apx. version 1.0.10 or 1.1.0)." - ), - DeprecationWarning - ) - query = workfiles_info_graphql_query(fields) for attr, filter_value in filters.items(): @@ -115,13 +103,13 @@ def get_workfiles_info( self._convert_entity_data(workfile_info) yield workfile_info - def get_workfile_info( + def get_workfile_entity( self, project_name: str, task_id: str, path: str, + *, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, ) -> Optional[WorkfileInfoDict]: """Workfile info entity by task id and workfile path. @@ -132,8 +120,6 @@ def get_workfile_info( fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. Returns: Optional[WorkfileInfoDict]: Workfile info entity or None. @@ -142,22 +128,21 @@ def get_workfile_info( if not task_id or not path: return None - for workfile_info in self.get_workfiles_info( + for workfile_info in self.get_workfile_entities( project_name, task_ids=[task_id], paths=[path], fields=fields, - own_attributes=own_attributes ): return workfile_info return None - def get_workfile_info_by_id( + def get_workfile_entity_by_id( self, project_name: str, workfile_id: str, + *, fields: Optional[Iterable[str]] = None, - own_attributes=_PLACEHOLDER, ) -> Optional[WorkfileInfoDict]: """Workfile info entity by id. @@ -167,8 +152,6 @@ def get_workfile_info_by_id( fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. - own_attributes (Optional[bool]): DEPRECATED: Not supported for - workfiles. Returns: Optional[WorkfileInfoDict]: Workfile info entity or None. @@ -177,11 +160,10 @@ def get_workfile_info_by_id( if not workfile_id: return None - for workfile_info in self.get_workfiles_info( + for workfile_info in self.get_workfile_entities( project_name, workfile_ids=[workfile_id], fields=fields, - own_attributes=own_attributes ): return workfile_info return None @@ -252,27 +234,11 @@ def create_workfile_entity( response.raise_for_status() return workfile_id - def delete_workfile_info( - self, - project_name: str, - workfile_id: str, - ) -> None: - """Delete workfile entity on server. - - Args: - project_name (str): Project name. - workfile_id (str): Workfile id to delete. - - """ - response = self.delete( - f"projects/{project_name}/workfiles/{workfile_id}" - ) - response.raise_for_status() - - def update_workfile_info( + def update_workfile_entity( self, project_name: str, workfile_id: str, + *, path: Optional[str] = None, task_id: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, @@ -330,3 +296,226 @@ def update_workfile_info( **update_data ) response.raise_for_status() + + def delete_workfile_entity( + self, + project_name: str, + workfile_id: str, + ) -> None: + """Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + response = self.delete( + f"projects/{project_name}/workfiles/{workfile_id}" + ) + response.raise_for_status() + + # --- DEPRECATED --- + def get_workfiles_info( + self, + project_name: str, + workfile_ids: Optional[Iterable[str]] = None, + task_ids: Optional[Iterable[str]] =None, + paths: Optional[Iterable[str]] =None, + path_regex: Optional[str] = None, + statuses: Optional[Iterable[str]] = None, + tags: Optional[Iterable[str]] = None, + has_links: Optional[str]=None, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Generator[WorkfileInfoDict, None, None]: + """DEPRECATED Workfile info entities by passed filters. + + Args: + project_name (str): Project under which the entity is located. + workfile_ids (Optional[Iterable[str]]): Workfile ids. + task_ids (Optional[Iterable[str]]): Task ids. + paths (Optional[Iterable[str]]): Rootless workfiles paths. + path_regex (Optional[str]): Regex filter for workfile path. + statuses (Optional[Iterable[str]]): Workfile info statuses used + for filtering. + tags (Optional[Iterable[str]]): Workfile info tags used + for filtering. + has_links (Optional[Literal[IN, OUT, ANY]]): Filter + representations with IN/OUT/ANY links. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Generator[WorkfileInfoDict, None, None]: Queried workfile info + entites. + + """ + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for workfiles. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning, + stacklevel=2, + ) + + return self.get_workfile_entities( + project_name, + workfile_ids=workfile_ids, + task_ids=task_ids, + paths=paths, + path_regex=path_regex, + statuses=statuses, + tags=tags, + has_links=has_links, + fields=fields, + ) + + def get_workfile_info( + self, + project_name: str, + task_id: str, + path: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[WorkfileInfoDict]: + """DEPRECATED Workfile info entity by task id and workfile path. + + Args: + project_name (str): Project under which the entity is located. + task_id (str): Task id. + path (str): Rootless workfile path. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for workfiles. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning, + stacklevel=2, + ) + + return self.get_workfile_entity( + project_name, task_id, path,fields=fields + ) + + def get_workfile_info_by_id( + self, + project_name: str, + workfile_id: str, + fields: Optional[Iterable[str]] = None, + own_attributes=_PLACEHOLDER, + ) -> Optional[WorkfileInfoDict]: + """DEPRECATED Workfile info entity by id. + + Args: + project_name (str): Project under which the entity is located. + workfile_id (str): Workfile info id. + fields (Optional[Iterable[str]]): Fields to be queried for + representation. All possible fields are returned if 'None' is + passed. + own_attributes (Optional[bool]): DEPRECATED: Not supported for + workfiles. + + Returns: + Optional[WorkfileInfoDict]: Workfile info entity or None. + + """ + if own_attributes is not _PLACEHOLDER: + warnings.warn( + ( + "'own_attributes' is not supported for workfiles. The" + " argument will be removed form function signature in" + " future (apx. version 1.0.10 or 1.1.0)." + ), + DeprecationWarning, + stacklevel=2, + ) + return self.get_workfile_entity_by_id( + project_name, + workfile_id, + fields=fields, + ) + + def update_workfile_info( + self, + project_name: str, + workfile_id: str, + path: Optional[str] = None, + task_id: Optional[str] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[Iterable[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = NOT_SET, + created_by: Optional[str] = None, + updated_by: Optional[str] = None, + ) -> None: + """DEPRECATED Update workfile entity on server. + + Update of ``attrib`` does change only passed attributes. If you want + to unset value, use ``None``. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id. + path (Optional[str]): New rootless workfile path.. + task_id (Optional[str]): New parent task id. + attrib (Optional[dict[str, Any]]): New attributes. + data (Optional[dict[str, Any]]): New data. + tags (Optional[Iterable[str]]): New tags. + status (Optional[str]): New status. + active (Optional[bool]): New active state. + thumbnail_id (Optional[str]): New thumbnail id. + created_by (Optional[str]): New created by username. + updated_by (Optional[str]): New updated by username. + + """ + return self.update_workfile_entity( + project_name, + workfile_id, + path=path, + task_id=task_id, + attrib=attrib, + data=data, + tags=tags, + status=status, + active=active, + thumbnail_id=thumbnail_id, + created_by=created_by, + updated_by=updated_by, + ) + + def delete_workfile_info( + self, + project_name: str, + workfile_id: str, + ) -> None: + """DEPRECATED Delete workfile entity on server. + + Args: + project_name (str): Project name. + workfile_id (str): Workfile id to delete. + + """ + return self.delete_workfile_entity( + project_name, + workfile_id, + ) From c6551d3483ee6ee81e9cafabbb5cf89872d91f23 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:03:13 +0200 Subject: [PATCH 233/506] rename info to entity --- ayon_api/operations.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 5a514162e..aa03d26a8 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -340,7 +340,7 @@ def new_representation_entity( return output -def new_workfile_info( +def new_workfile_entity( filepath: str, task_id: str, status: Optional[str] = None, @@ -396,6 +396,28 @@ def new_workfile_info( return output +def new_workfile_info( + filepath: str, + task_id: str, + status: Optional[str] = None, + tags: Optional[list[str]] = None, + attribs: Optional[dict[str, Any]] = None, + description: Optional[str] = None, + data: Optional[dict[str, Any]] = None, + entity_id: Optional[str] = None, +) -> NewWorkfileDict: + return new_workfile_entity( + filepath, + task_id, + status, + tags, + attribs, + description, + data, + entity_id, + ) + + class AbstractOperation(ABC): """Base operation class. @@ -1545,7 +1567,7 @@ def delete_representation( project_name, "representation", representation_id ) - def create_workfile_info( + def create_workfile_entity( self, project_name: str, path: str, @@ -1603,7 +1625,7 @@ def create_workfile_info( create_data ) - def update_workfile_info( + def update_workfile_entity( self, project_name: str, workfile_id: str, From 16b1e1e533d29d321bd71a93a435f880761c7e35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:04:39 +0200 Subject: [PATCH 234/506] add entity to workfile --- ayon_api/operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index aa03d26a8..c280c2bff 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1688,12 +1688,12 @@ def update_workfile_entity( update_data ) - def delete_workfile( + def delete_workfile_entity( self, project_name: str, workfile_id: str, ) -> DeleteOperation: - """Delete representation. + """Delete workfile entity. Args: project_name (str): Project name. From 88408a9f1b125a29e10f8c5cb529d9932557fe4c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:04:47 +0200 Subject: [PATCH 235/506] change docstrings --- ayon_api/operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index c280c2bff..9f68d00c4 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1581,7 +1581,7 @@ def create_workfile_entity( active: Optional[bool] = None, workfile_id: Optional[str] = None, ) -> CreateOperation: - """Create new workfile. + """Create new workfile entity. Args: project_name (str): Project name. @@ -1640,7 +1640,7 @@ def update_workfile_entity( created_by: Optional[str] = None, updated_by: Optional[str] = None, ) -> UpdateOperation: - """Update workfile info entity on server. + """Update workfile entity on server. Update of ``data`` will override existing value on folder entity. From fa07ff098bfeb58c11b93ad8d7fe9d19b5d19b45 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:16:02 +0200 Subject: [PATCH 236/506] bump version to '1.2.2' --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 552e1099e..9237754e5 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.2-dev" +__version__ = "1.2.2" diff --git a/pyproject.toml b/pyproject.toml index e2a6eb937..199d164be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.2-dev" +version = "1.2.2" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.2-dev" +version = "1.2.2" description = "AYON Python API" authors = [ "ynput.io " From c3de3397a9314b46d37280710c4b82ea24f2a416 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 10 Oct 2025 17:16:47 +0200 Subject: [PATCH 237/506] bump version to '1.2.3-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 9237754e5..74e9149d5 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.2" +__version__ = "1.2.3-dev" diff --git a/pyproject.toml b/pyproject.toml index 199d164be..ae5d97fc2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.2" +version = "1.2.3-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From b4ece4adf929a3c58f4b5e8d9324c38bf8e9cacb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Oct 2025 17:38:35 +0200 Subject: [PATCH 238/506] fix correct object --- ayon_api/graphql.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index 771752364..d377d94b0 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -857,8 +857,8 @@ def add_obj_edge_field(self, field: BaseGraphQlQueryField) -> None: self._edge_children.append(field) field.set_parent(self) - def add_edge_field(self, name: str) -> GraphQlQueryEdgeField: - item = GraphQlQueryEdgeField(name, self, self._order) + def add_edge_field(self, name: str) -> GraphQlQueryField: + item = GraphQlQueryField(name, self, self._order) self.add_obj_edge_field(item) return item From ec40f8c9b9eb06e3f7b2226754d224f01c398699 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:12:34 +0200 Subject: [PATCH 239/506] fetch both active and inactive entities --- ayon_api/entity_hub.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 5e52ef8f1..f12c00776 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -283,14 +283,14 @@ def get_or_fetch_entity_by_id( self.project_name, entity_id, fields=self._get_folder_fields(), - own_attributes=True + own_attributes=True, ) elif entity_type == "task": entity_data = self._connection.get_task_by_id( self.project_name, entity_id, fields=self._get_task_fields(), - own_attributes=True + own_attributes=True, ) elif entity_type == "product": entity_data = self._connection.get_product_by_id( @@ -781,6 +781,7 @@ def _fetch_entity_children(self, entity: BaseEntity) -> None: parent_ids=[entity.id], fields=folder_fields, own_attributes=True, + active=None, )) elif entity.entity_type == "folder": @@ -789,6 +790,7 @@ def _fetch_entity_children(self, entity: BaseEntity) -> None: parent_ids=[entity.id], fields=folder_fields, own_attributes=True, + active=None, )) tasks = list(self._connection.get_tasks( @@ -796,6 +798,7 @@ def _fetch_entity_children(self, entity: BaseEntity) -> None: folder_ids=[entity.id], fields=task_fields, own_attributes=True, + active=None, )) children_ids = { @@ -897,7 +900,7 @@ def fill_project_from_server(self) -> ProjectEntity: project_name = self.project_name project = self._connection.get_project( project_name, - own_attributes=True + own_attributes=True, ) if not project: raise ValueError(f"Project \"{project_name}\" was not found.") @@ -949,11 +952,13 @@ def fetch_hierarchy_entities(self) -> None: project_entity.name, fields=folder_fields, own_attributes=True, + active=None, ) tasks = self._connection.get_tasks( project_entity.name, fields=task_fields, own_attributes=True, + active=None, ) folders_by_parent_id = collections.defaultdict(list) for folder in folders: From 83b0e7e00955490e0450864b97af38f8c513a75f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:28:53 +0200 Subject: [PATCH 240/506] bump version to 1.2.3 --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 74e9149d5..6842a7353 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.3-dev" +__version__ = "1.2.3" diff --git a/pyproject.toml b/pyproject.toml index ae5d97fc2..756345ef9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.3-dev" +version = "1.2.3" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 5c93a71b30a6f269c8d87c30cecb2f5825d5d846 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 17 Oct 2025 16:29:38 +0200 Subject: [PATCH 241/506] bump version to '1.2.4-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 6842a7353..737c6e13e 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.3" +__version__ = "1.2.4-dev" diff --git a/pyproject.toml b/pyproject.toml index 756345ef9..61eb44d7e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.3" +version = "1.2.4-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 4bf7368efb311531ce80629bb2a6745db189f041 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 21 Oct 2025 14:18:21 +0200 Subject: [PATCH 242/506] add support for items fetching in links query --- ayon_api/constants.py | 4 ++++ ayon_api/graphql_queries.py | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 6dada2de5..1bfd14c99 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -252,4 +252,8 @@ "tags", "updatedAt", "updatedBy", + "items.id", + "items.entityId", + "items.entityType", + "items.position", } diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 38db44672..be5e39673 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -680,9 +680,25 @@ def entity_lists_graphql_query(fields): entity_lists_field = project_field.add_field_with_edges("entityLists") entity_lists_field.set_filter("ids", entity_list_ids) - nested_fields = fields_to_dict(set(fields)) + fields = set(fields) + items_field_names = set() + for field_name in set(fields): + field_name.removeprefix("items") + if not field_name.startswith("items"): + continue + + fields.discard(field_name) + field_name = field_name.removeprefix("items").lstrip(".") + if field_name: + items_field_names.add(field_name) query_queue = collections.deque() + if items_field_names: + items_field = entity_lists_field.add_field_with_edges("items") + for field_name in items_field_names: + items_field.add_edge_field(field_name) + + nested_fields = fields_to_dict(set(fields)) for key, value in nested_fields.items(): query_queue.append((key, value, entity_lists_field)) From a56d7ead482e71aa9c68bbce13329cc5bf058d1f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:40:40 +0200 Subject: [PATCH 243/506] implemented new_task_entity --- ayon_api/operations.py | 68 ++++++++++++++++++++++++++++++++++++++++++ ayon_api/typing.py | 15 ++++++++++ 2 files changed, 83 insertions(+) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 9f68d00c4..8b91340ca 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -15,6 +15,7 @@ from .server_api import ServerAPI from .typing import ( NewFolderDict, + NewTaskDict, NewProductDict, NewVersionDict, NewRepresentationDict, @@ -76,6 +77,7 @@ def new_folder_entity( folder_type: str, parent_id: Optional[str] = None, status: Optional[str] = None, + active: Optional[bool] = None, tags: Optional[list[str]] = None, attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, @@ -89,6 +91,7 @@ def new_folder_entity( folder_type (str): Type of folder. parent_id (Optional[str]): Parent folder id. status (Optional[str]): Product status. + active (Optional[bool]): Active status.. tags (Optional[list[str]]): List of tags. attribs (Optional[dict[str, Any]]): Explicitly set attributes of folder. @@ -125,6 +128,67 @@ def new_folder_entity( output["status"] = status if tags: output["tags"] = tags + if active is not None: + output["active"] = active + return output + + +def new_task_entity( + name: str, + task_type: str, + folder_id: str, + *, + label: Optional[str] = None, + assignees: Optional[list[str]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, + tags: Optional[list[str]] = None, + status: Optional[str] = None, + active: Optional[bool] = None, + thumbnail_id: Optional[str] = None, + task_id: Optional[str] = None, +) -> NewTaskDict: + """Create skeleton data of task entity. + + Args: + name (str): Folder name. + task_type (str): Task type. + folder_id (str): Parent folder id. + label (Optional[str]): Label of folder. + assignees (Optional[list[str]]): Task assignees. + attrib (Optional[dict[str, Any]]): Task attributes. + data (Optional[dict[str, Any]]): Task data. + tags (Optional[list[str]]): Task tags. + status (Optional[str]): Task status. + active (Optional[bool]): Task active state. + thumbnail_id (Optional[str]): Task thumbnail id. + task_id (Optional[str]): Task id. If not passed new id is + generated. + + Returns: + NewTaskDict: Skeleton of task entity. + + """ + if not task_id: + task_id = create_entity_id() + output = { + "id": task_id, + "name": name, + "taskType": task_type, + "folderId": folder_id, + } + for key, value in ( + ("label", label), + ("attrib", attrib), + ("data", data), + ("tags", tags), + ("status", status), + ("assignees", assignees), + ("active", active), + ("thumbnailId", thumbnail_id), + ): + if value is not None: + output[key] = value return output @@ -1046,6 +1110,10 @@ def create_task( "taskType": task_type, "folderId": folder_id, } + if tags is not None: + tags = list(tags) + if assignees is not None: + assignees = list(assignees) for key, value in ( ("label", label), ("attrib", attrib), diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 24458cebb..e4f6c74ba 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -362,6 +362,21 @@ class NewFolderDict(TypedDict): tags: NotRequired[list[str]] +class NewTaskDict(TypedDict): + id: str + name: str + task_type: str + folder_id: str + label: NotRequired[str] + assignees: NotRequired[list[str]] + attrib: NotRequired[dict[str, Any]] + data: NotRequired[dict[str, Any]] + thumbnailId: NotRequired[str] + active: NotRequired[bool] + status: NotRequired[str] + tags: NotRequired[list[str]] + + class NewProductDict(TypedDict): id: str name: str From 8375b93262fe779e11931880b0d2471c9adfdcba Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:51:15 +0100 Subject: [PATCH 244/506] added functions to create thumbnail from stream --- ayon_api/__init__.py | 4 ++ ayon_api/_api.py | 52 +++++++++++++++++++++ ayon_api/_api_helpers/base.py | 11 +++++ ayon_api/_api_helpers/thumbnails.py | 72 +++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 7cf0825f1..b28083a66 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -271,7 +271,9 @@ get_version_thumbnail, get_workfile_thumbnail, create_thumbnail, + create_thumbnail_with_stream, update_thumbnail, + update_thumbnail_from_stream, ) @@ -546,5 +548,7 @@ "get_version_thumbnail", "get_workfile_thumbnail", "create_thumbnail", + "create_thumbnail_with_stream", "update_thumbnail", + "update_thumbnail_from_stream", ) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 0d93e03e0..b24a715c8 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -7726,6 +7726,34 @@ def create_thumbnail( ) +def create_thumbnail_with_stream( + project_name: str, + stream: StreamType, + thumbnail_id: Optional[str] = None, +) -> str: + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + stream (StreamType): Thumbnail content stream. + thumbnail_id (Optional[str]): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + con = get_server_api_connection() + return con.create_thumbnail_with_stream( + project_name=project_name, + stream=stream, + thumbnail_id=thumbnail_id, + ) + + def update_thumbnail( project_name: str, thumbnail_id: str, @@ -7751,3 +7779,27 @@ def update_thumbnail( thumbnail_id=thumbnail_id, src_filepath=src_filepath, ) + + +def update_thumbnail_from_stream( + project_name: str, + thumbnail_id: str, + stream: StreamType, +) -> None: + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + stream (StreamType): Thumbnail content stream. + + """ + con = get_server_api_connection() + return con.update_thumbnail_from_stream( + project_name=project_name, + thumbnail_id=thumbnail_id, + stream=stream, + ) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index 3e7499506..4d2d2e00c 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -13,6 +13,7 @@ AnyEntityDict, ServerVersion, ProjectDict, + StreamType, ) _PLACEHOLDER = object() @@ -84,6 +85,16 @@ def upload_file( ) -> requests.Response: raise NotImplementedError() + def upload_file_from_stream( + self, + endpoint: str, + stream: StreamType, + progress: Optional[TransferProgress] = None, + request_type: Optional[RequestType] = None, + **kwargs + ) -> requests.Response: + raise NotImplementedError() + def download_file( self, endpoint: str, diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index e4b1c56d1..2a0e12684 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -5,6 +5,7 @@ from typing import Optional from ayon_api.utils import ( + get_media_mime_type_for_stream, get_media_mime_type, ThumbnailContent, RequestTypes, @@ -259,6 +260,48 @@ def create_thumbnail( response.raise_for_status() return response.json()["id"] + def create_thumbnail_with_stream( + self, + project_name: str, + stream: StreamType, + thumbnail_id: Optional[str] = None, + ) -> str: + """Create new thumbnail on server from passed path. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + stream (StreamType): Thumbnail content stream. + thumbnail_id (Optional[str]): Prepared if of thumbnail. + + Returns: + str: Created thumbnail id. + + Raises: + ValueError: When thumbnail source cannot be processed. + + """ + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + if thumbnail_id: + self.update_thumbnail_from_stream( + project_name, + thumbnail_id, + stream + ) + return thumbnail_id + + mime_type = get_media_mime_type_for_stream(stream) + response = self.upload_file_from_stream( + f"projects/{project_name}/thumbnails", + stream, + request_type=RequestTypes.post, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + return response.json()["id"] + def update_thumbnail( self, project_name: str, thumbnail_id: str, src_filepath: str ) -> None: @@ -288,6 +331,35 @@ def update_thumbnail( ) response.raise_for_status() + def update_thumbnail_from_stream( + self, + project_name: str, + thumbnail_id: str, + stream: StreamType, + ) -> None: + """Change thumbnail content by id. + + Update can be also used to create new thumbnail. + + Args: + project_name (str): Project where the thumbnail will be created + and can be used. + thumbnail_id (str): Thumbnail id to update. + stream (StreamType): Thumbnail content stream. + + """ + if not os.path.exists(src_filepath): + raise ValueError("Entered filepath does not exist.") + + mime_type = get_media_mime_type_for_stream(src_filepath) + response = self.upload_file_from_stream( + f"projects/{project_name}/thumbnails/{thumbnail_id}", + stream, + request_type=RequestTypes.put, + headers={"Content-Type": mime_type}, + ) + response.raise_for_status() + def _prepare_thumbnail_content( self, project_name: str, From e9254ceb1233681691d20b230a5bc98309eba22f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:14:03 +0100 Subject: [PATCH 245/506] fix copy pasted code --- ayon_api/_api_helpers/thumbnails.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 2a0e12684..6a0e69c8c 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -266,7 +266,7 @@ def create_thumbnail_with_stream( stream: StreamType, thumbnail_id: Optional[str] = None, ) -> str: - """Create new thumbnail on server from passed path. + """Create new thumbnail on server from byte stream. Args: project_name (str): Project where the thumbnail will be created @@ -278,12 +278,9 @@ def create_thumbnail_with_stream( str: Created thumbnail id. Raises: - ValueError: When thumbnail source cannot be processed. + ValueError: When a thumbnail source cannot be processed. """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") - if thumbnail_id: self.update_thumbnail_from_stream( project_name, @@ -348,10 +345,7 @@ def update_thumbnail_from_stream( stream (StreamType): Thumbnail content stream. """ - if not os.path.exists(src_filepath): - raise ValueError("Entered filepath does not exist.") - - mime_type = get_media_mime_type_for_stream(src_filepath) + mime_type = get_media_mime_type_for_stream(stream) response = self.upload_file_from_stream( f"projects/{project_name}/thumbnails/{thumbnail_id}", stream, From 48f9f50f3b55f83a46735867284ffc33b942baf0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:15:08 +0100 Subject: [PATCH 246/506] add missing import --- ayon_api/_api_helpers/thumbnails.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 6a0e69c8c..577536e28 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -2,6 +2,7 @@ import os import warnings +import typing from typing import Optional from ayon_api.utils import ( @@ -14,6 +15,9 @@ from .base import BaseServerAPI +if typing.TYPE_CHECKING: + from .typing import StreamType + class ThumbnailsAPI(BaseServerAPI): def get_thumbnail_by_id( From c8c136e49f7fd722a32140d79e53f8304d53cd50 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Oct 2025 15:15:20 +0100 Subject: [PATCH 247/506] update public api --- ayon_api/_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index b24a715c8..be6b21d9c 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -7731,7 +7731,7 @@ def create_thumbnail_with_stream( stream: StreamType, thumbnail_id: Optional[str] = None, ) -> str: - """Create new thumbnail on server from passed path. + """Create new thumbnail on server from byte stream. Args: project_name (str): Project where the thumbnail will be created @@ -7743,7 +7743,7 @@ def create_thumbnail_with_stream( str: Created thumbnail id. Raises: - ValueError: When thumbnail source cannot be processed. + ValueError: When a thumbnail source cannot be processed. """ con = get_server_api_connection() From 67ac162cda13f1e216a6df1d59c3b48b8658cf08 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:28:45 +0100 Subject: [PATCH 248/506] bump version to '1.2.4' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 737c6e13e..49d2c2902 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.4-dev" +__version__ = "1.2.4" diff --git a/pyproject.toml b/pyproject.toml index 61eb44d7e..286d9e3d5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.4-dev" +version = "1.2.4" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From ae711366be36b362ba9410dc2c4930b80135e05d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Oct 2025 19:29:52 +0100 Subject: [PATCH 249/506] bump version to '1.2.5-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 49d2c2902..63dfba578 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.4" +__version__ = "1.2.5-dev" diff --git a/pyproject.toml b/pyproject.toml index 286d9e3d5..cfecaa78c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.4" +version = "1.2.5-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 10e49d7c229cde43fed745c535fc16795633351f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 10 Nov 2025 15:03:53 +0100 Subject: [PATCH 250/506] :recycle: add server version for product base types --- ayon_api/server_api.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index aae4f5097..8010a239d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -916,11 +916,9 @@ def product_base_type_supported(self) -> bool: """Product base types are available on server.""" if self._product_base_type_supported is None: major, minor, patch, _, _ = self.server_version_tuple - self._product_base_type_supported = False - # TODO implement when server version of the support is known - # self._product_base_type_supported = ( - # (major, minor, patch) >= (1, 12, 0) - # ) + self._product_base_type_supported = ( + (major, minor, patch) >= (1, 13, 0) + ) return self._product_base_type_supported def _get_user_info(self) -> Optional[dict[str, Any]]: From e01887ea7eafee91b49b7d6292d3e35637869f63 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:09:18 +0100 Subject: [PATCH 251/506] better definition if projects requires graphql or rest --- ayon_api/_api_helpers/base.py | 6 +++ ayon_api/_api_helpers/projects.py | 78 ++++++++++++++++++++++++------- 2 files changed, 67 insertions(+), 17 deletions(-) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index 4d2d2e00c..d7733ce64 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -14,6 +14,7 @@ ServerVersion, ProjectDict, StreamType, + AttributeScope, ) _PLACEHOLDER = object() @@ -125,6 +126,11 @@ def get_user( ) -> Optional[dict[str, Any]]: raise NotImplementedError() + def get_attributes_fields_for_type( + self, entity_type: AttributeScope + ) -> set[str]: + raise NotImplementedError() + def _prepare_fields( self, entity_type: str, diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 09fe66d98..ff44dfbcc 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -3,6 +3,7 @@ import json import platform import warnings +from enum import Enum import typing from typing import Optional, Generator, Iterable, Any @@ -16,6 +17,12 @@ from ayon_api.typing import ProjectDict, AnatomyPresetDict +class ProjectFetchType(Enum): + GraphQl = "GraphQl" + REST = "REST" + Both = "Both" + + class ProjectsAPI(BaseServerAPI): def get_project_anatomy_presets(self) -> list[AnatomyPresetDict]: """Anatomy presets available on server. @@ -218,7 +225,7 @@ def get_projects( if fields is not None: fields = set(fields) - graphql_fields, use_rest = self._get_project_graphql_fields(fields) + graphql_fields, fetch_type = self._get_project_graphql_fields(fields) projects_by_name = {} if graphql_fields: projects = list(self._get_graphql_projects( @@ -227,7 +234,7 @@ def get_projects( fields=graphql_fields, own_attributes=own_attributes, )) - if not use_rest: + if fetch_type == ProjectFetchType.GraphQl: yield from projects return projects_by_name = {p["name"]: p for p in projects} @@ -262,7 +269,7 @@ def get_project( if fields is not None: fields = set(fields) - graphql_fields, use_rest = self._get_project_graphql_fields(fields) + graphql_fields, fetch_type = self._get_project_graphql_fields(fields) graphql_project = None if graphql_fields: graphql_project = next(self._get_graphql_projects( @@ -271,7 +278,7 @@ def get_project( fields=graphql_fields, own_attributes=own_attributes, ), None) - if not graphql_project or not use_rest: + if not graphql_project or fetch_type == fetch_type.GraphQl: return graphql_project project = self.get_rest_project(project_name) @@ -585,34 +592,71 @@ def get_project_roots_by_platform( def _get_project_graphql_fields( self, fields: Optional[set[str]] - ) -> tuple[set[str], bool]: - """Fetch of project must be done using REST endpoint. + ) -> tuple[set[str], ProjectFetchType]: + """Find out if project can be fetched with GraphQl, REST or both. Returns: set[str]: GraphQl fields. """ if fields is None: - return set(), True + return set(), ProjectFetchType.REST has_product_types = False graphql_fields = set() - for field in fields: + for field in tuple(fields): # Product types are available only in GraphQl - if field.startswith("productTypes"): + if field == "productTypes": + has_product_types = True + fields.discard(field) + graphql_fields.add("productTypes.name") + graphql_fields.add("productTypes.icon") + graphql_fields.add("productTypes.color") + + elif field.startswith("productTypes"): has_product_types = True graphql_fields.add(field) - if not has_product_types: - return set(), True + elif field == "productBaseTypes": + has_product_types = True + fields.discard(field) + graphql_fields.add("productBaseTypes.name") - inters = fields & {"name", "code", "active", "library"} + elif field.startswith("productBaseTypes"): + has_product_types = True + graphql_fields.add("productBaseTypes.name") + + elif field == "bundles": + fields.discard("bundles") + graphql_fields.add("bundles.production") + graphql_fields.add("bundles.staging") + + elif field == "attrib": + fields.discard("attrib") + graphql_fields |= self.get_attributes_fields_for_type( + "project" + ) + + inters = fields & { + "name", + "code", + "active", + "library", + "usedTags", + "data", + } remainders = fields - (inters | graphql_fields) - if remainders: - graphql_fields.add("name") - return graphql_fields, True - graphql_fields |= inters - return graphql_fields, False + if not remainders: + graphql_fields |= inters + return graphql_fields, ProjectFetchType.GraphQl + + graphql_fields.add("name") + fetch_type = ( + ProjectFetchType.Both + if has_product_types + else ProjectFetchType.REST + ) + return graphql_fields, fetch_type def _fill_project_entity_data(self, project: dict[str, Any]) -> None: # Add fake scope to statuses if not available From 40517ceec740968202680212b74fd4b03a705f59 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:16:05 +0100 Subject: [PATCH 252/506] implemented get_rest_projects_list --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 26 ++++++++++++++++ ayon_api/_api_helpers/projects.py | 49 ++++++++++++++++++++++++------- ayon_api/typing.py | 8 +++++ 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index b28083a66..ca8721284 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -152,6 +152,7 @@ get_build_in_anatomy_preset, get_rest_project, get_rest_projects, + get_rest_projects_list, get_project_names, get_projects, get_project, @@ -429,6 +430,7 @@ "get_build_in_anatomy_preset", "get_rest_project", "get_rest_projects", + "get_rest_projects_list", "get_project_names", "get_projects", "get_project", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index be6b21d9c..d6dce707c 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -64,6 +64,7 @@ BundlesInfoDict, AnatomyPresetDict, SecretDict, + ProjectListDict, AnyEntityDict, ProjectDict, FolderDict, @@ -3573,6 +3574,31 @@ def get_rest_projects( ) +def get_rest_projects_list( + active: Optional[bool] = True, + library: Optional[bool] = None, +) -> list[ProjectListDict]: + """Receive available projects. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + list[ProjectListDict]: List of available projects. + + """ + con = get_server_api_connection() + return con.get_rest_projects_list( + active=active, + library=library, + ) + + def get_project_names( active: Optional[bool] = True, library: Optional[bool] = None, diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index ff44dfbcc..a0e9bf702 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -14,7 +14,11 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from ayon_api.typing import ProjectDict, AnatomyPresetDict + from ayon_api.typing import ( + ProjectDict, + AnatomyPresetDict, + ProjectListDict, + ) class ProjectFetchType(Enum): @@ -163,12 +167,12 @@ def get_rest_projects( if project: yield project - def get_project_names( + def get_rest_projects_list( self, active: Optional[bool] = True, library: Optional[bool] = None, - ) -> list[str]: - """Receive available project names. + ) -> list[ProjectListDict]: + """Receive available projects. User must be logged in. @@ -179,7 +183,7 @@ def get_project_names( are returned if 'None' is passed. Returns: - list[str]: List of available project names. + list[ProjectListDict]: List of available projects. """ if active is not None: @@ -188,16 +192,41 @@ def get_project_names( if library is not None: library = "true" if library else "false" - query = prepare_query_string({"active": active, "library": library}) + query = prepare_query_string({ + "active": active, + "library": library, + }) response = self.get(f"projects{query}") response.raise_for_status() data = response.data - project_names = [] if data: - for project in data["projects"]: - project_names.append(project["name"]) - return project_names + return data["projects"] + return [] + + def get_project_names( + self, + active: Optional[bool] = True, + library: Optional[bool] = None, + ) -> list[str]: + """Receive available project names. + + User must be logged in. + + Args: + active (Optional[bool]): Filter active/inactive projects. Both + are returned if 'None' is passed. + library (Optional[bool]): Filter standard/library projects. Both + are returned if 'None' is passed. + + Returns: + list[str]: List of available project names. + + """ + return [ + project["name"] + for project in self.get_rest_projects_list(active, library) + ] def get_projects( self, diff --git a/ayon_api/typing.py b/ayon_api/typing.py index e4f6c74ba..008490093 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -328,6 +328,14 @@ class SecretDict(TypedDict): value: str +class ProjectListDict(TypedDict): + name: str + code: str + active: bool + createdAt: str + updatedAt: str + + ProjectDict = dict[str, Any] FolderDict = dict[str, Any] TaskDict = dict[str, Any] From e0ede75b54ae68a04fdeb735a9f21f7e52bac924 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:49:47 +0100 Subject: [PATCH 253/506] allow to use projects list if specific fields are requested --- ayon_api/_api_helpers/projects.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index a0e9bf702..d831ee009 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -24,7 +24,8 @@ class ProjectFetchType(Enum): GraphQl = "GraphQl" REST = "REST" - Both = "Both" + RESTList = "RESTList" + GraphQlAndREST = "GraphQlAndREST" class ProjectsAPI(BaseServerAPI): @@ -255,6 +256,9 @@ def get_projects( fields = set(fields) graphql_fields, fetch_type = self._get_project_graphql_fields(fields) + if fetch_type == ProjectFetchType.RESTList: + return self.get_rest_projects_list(active, library) + projects_by_name = {} if graphql_fields: projects = list(self._get_graphql_projects( @@ -631,8 +635,18 @@ def _get_project_graphql_fields( if fields is None: return set(), ProjectFetchType.REST - has_product_types = False + rest_list_fields = { + "name", + "code", + "active", + "createdAt", + "updatedAt", + } graphql_fields = set() + if len(fields - rest_list_fields) == 0: + return graphql_fields, ProjectFetchType.RESTList + + has_product_types = False for field in tuple(fields): # Product types are available only in GraphQl if field == "productTypes": From 98b546e415c3090f2fba89861277ebb25851b288 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:50:05 +0100 Subject: [PATCH 254/506] fix attribute error --- ayon_api/_api_helpers/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index d831ee009..0c875c3a6 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -695,7 +695,7 @@ def _get_project_graphql_fields( graphql_fields.add("name") fetch_type = ( - ProjectFetchType.Both + ProjectFetchType.GraphQlAndREST if has_product_types else ProjectFetchType.REST ) From 9d386687163139709e88f7d409a722854ebcca93 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 11 Nov 2025 17:13:41 +0100 Subject: [PATCH 255/506] fix None data on projects --- ayon_api/_api_helpers/projects.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 0c875c3a6..c6e19e668 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -719,7 +719,9 @@ def _fill_project_entity_data(self, project: dict[str, Any]) -> None: # Convert 'data' from string to dict if needed if "data" in project: project_data = project["data"] - if isinstance(project_data, str): + if project_data is None: + project["data"] = {} + elif isinstance(project_data, str): project_data = json.loads(project_data) project["data"] = project_data From a98bce0608fba3d24a9de4c1c40e6d1d33a822e4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Nov 2025 11:48:29 +0100 Subject: [PATCH 256/506] few more fixes --- ayon_api/_api_helpers/projects.py | 48 +++++++++++++++++-------------- 1 file changed, 26 insertions(+), 22 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index c6e19e668..b6b15e699 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -197,13 +197,10 @@ def get_rest_projects_list( "active": active, "library": library, }) - response = self.get(f"projects{query}") response.raise_for_status() data = response.data - if data: - return data["projects"] - return [] + return data["projects"] def get_project_names( self, @@ -257,7 +254,8 @@ def get_projects( graphql_fields, fetch_type = self._get_project_graphql_fields(fields) if fetch_type == ProjectFetchType.RESTList: - return self.get_rest_projects_list(active, library) + yield from self.get_rest_projects_list(active, library) + return projects_by_name = {} if graphql_fields: @@ -275,7 +273,7 @@ def get_projects( for project in self.get_rest_projects(active, library): name = project["name"] graphql_p = projects_by_name.get(name) - if graphql_p: + if graphql_p and "productTypes" in graphql_p: project["productTypes"] = graphql_p["productTypes"] yield project @@ -667,12 +665,15 @@ def _get_project_graphql_fields( elif field.startswith("productBaseTypes"): has_product_types = True - graphql_fields.add("productBaseTypes.name") + graphql_fields.add(field) + + elif field == "bundle" or field == "bundles": + fields.discard(field) + graphql_fields.add("bundle.production") + graphql_fields.add("bundle.staging") - elif field == "bundles": - fields.discard("bundles") - graphql_fields.add("bundles.production") - graphql_fields.add("bundles.staging") + elif field.startswith("bundle"): + graphql_fields.add(field) elif field == "attrib": fields.discard("attrib") @@ -680,6 +681,8 @@ def _get_project_graphql_fields( "project" ) + # NOTE 'config' in GraphQl is NOT the same as from REST api. + # - At the moment of this comment there is missing 'productBaseTypes'. inters = fields & { "name", "code", @@ -693,13 +696,11 @@ def _get_project_graphql_fields( graphql_fields |= inters return graphql_fields, ProjectFetchType.GraphQl - graphql_fields.add("name") - fetch_type = ( - ProjectFetchType.GraphQlAndREST - if has_product_types - else ProjectFetchType.REST - ) - return graphql_fields, fetch_type + if has_product_types: + graphql_fields.add("name") + return graphql_fields, ProjectFetchType.GraphQlAndREST + + return set(), ProjectFetchType.REST def _fill_project_entity_data(self, project: dict[str, Any]) -> None: # Add fake scope to statuses if not available @@ -727,7 +728,7 @@ def _fill_project_entity_data(self, project: dict[str, Any]) -> None: # Fill 'bundle' from data if is not filled if "bundle" not in project: - bundle_data = project["data"].get("bundle", {}) + bundle_data = project["data"].get("bundle") or {} prod_bundle = bundle_data.get("production") staging_bundle = bundle_data.get("staging") project["bundle"] = { @@ -736,9 +737,12 @@ def _fill_project_entity_data(self, project: dict[str, Any]) -> None: } # Convert 'config' from string to dict if needed - config = project.get("config") - if isinstance(config, str): - project["config"] = json.loads(config) + if "config" in project: + config = project["config"] + if config is None: + project["config"] = {} + elif isinstance(config, str): + project["config"] = json.loads(config) # Unifiy 'linkTypes' data structure from REST and GraphQL if "linkTypes" in project: From fab4d3cdcdda0339b35c0e625fd85fb7351a08c5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:35:25 +0100 Subject: [PATCH 257/506] added 'usedTags' to graphql fields --- ayon_api/_api_helpers/projects.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index b6b15e699..1703f9524 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -644,27 +644,29 @@ def _get_project_graphql_fields( if len(fields - rest_list_fields) == 0: return graphql_fields, ProjectFetchType.RESTList - has_product_types = False + must_use_graphql = False for field in tuple(fields): # Product types are available only in GraphQl - if field == "productTypes": - has_product_types = True + if field == "usedTags": + graphql_fields.add("usedTags") + elif field == "productTypes": + must_use_graphql = True fields.discard(field) graphql_fields.add("productTypes.name") graphql_fields.add("productTypes.icon") graphql_fields.add("productTypes.color") elif field.startswith("productTypes"): - has_product_types = True + must_use_graphql = True graphql_fields.add(field) elif field == "productBaseTypes": - has_product_types = True + must_use_graphql = True fields.discard(field) graphql_fields.add("productBaseTypes.name") elif field.startswith("productBaseTypes"): - has_product_types = True + must_use_graphql = True graphql_fields.add(field) elif field == "bundle" or field == "bundles": @@ -696,7 +698,7 @@ def _get_project_graphql_fields( graphql_fields |= inters return graphql_fields, ProjectFetchType.GraphQl - if has_product_types: + if must_use_graphql: graphql_fields.add("name") return graphql_fields, ProjectFetchType.GraphQlAndREST From 80376bb98f913b0e213b9086ba47af624c07fba9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 12 Nov 2025 14:15:38 +0100 Subject: [PATCH 258/506] fix optional graphql keys --- ayon_api/_api_helpers/projects.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 1703f9524..4daefe404 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -273,8 +273,13 @@ def get_projects( for project in self.get_rest_projects(active, library): name = project["name"] graphql_p = projects_by_name.get(name) - if graphql_p and "productTypes" in graphql_p: - project["productTypes"] = graphql_p["productTypes"] + if graphql_p: + for key in ( + "productTypes", + "usedTags", + ): + if key in graphql_p: + project[key] = graphql_p[key] yield project def get_project( @@ -316,7 +321,12 @@ def get_project( if own_attributes: fill_own_attribs(project) if graphql_project: - project["productTypes"] = graphql_project["productTypes"] + for key in ( + "productTypes", + "usedTags", + ): + if key in graphql_project: + project[key] = graphql_project[key] return project def create_project( From 5c04badbdefabee02468eb09ff6e49dc5b46bde3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= <33513211+antirotor@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:35:35 +0100 Subject: [PATCH 259/506] Update ayon_api/operations.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- ayon_api/operations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index de09a726d..dd49da7db 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -1299,7 +1299,7 @@ def update_product( """Update product entity on server. Update of ``data`` will override the existing value on - the folder entity. + the product entity. Update of ``attrib`` does change only passed attributes. If you want to unset value, use ``None``. From daef6b9972ceea2180ca7845aadd3d1e9dc893f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:45:05 +0100 Subject: [PATCH 260/506] rename 'product_base_type_supported' to 'is_product_base_type_supported' --- ayon_api/__init__.py | 4 ++-- ayon_api/_api.py | 4 ++-- ayon_api/_api_helpers/base.py | 2 +- ayon_api/_api_helpers/products.py | 6 +++--- ayon_api/entity_hub.py | 8 ++++---- ayon_api/server_api.py | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index dfda311d9..2369c97b1 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -54,7 +54,7 @@ get_info, get_server_version, get_server_version_tuple, - product_base_type_supported, + is_product_base_type_supported, get_users, get_user_by_name, get_user, @@ -332,7 +332,7 @@ "get_info", "get_server_version", "get_server_version_tuple", - "product_base_type_supported", + "is_product_base_type_supported", "get_users", "get_user_by_name", "get_user", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index a84b30d0f..ee0d0cb00 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -720,11 +720,11 @@ def get_server_version_tuple() -> ServerVersion: return con.get_server_version_tuple() -def product_base_type_supported() -> bool: +def is_product_base_type_supported() -> bool: """Product base types are available on server. """ con = get_server_api_connection() - return con.product_base_type_supported() + return con.is_product_base_type_supported() def get_users( diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index 825ed265c..822349909 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -24,7 +24,7 @@ class BaseServerAPI: def log(self) -> logging.Logger: raise NotImplementedError() - def product_base_type_supported(self) -> bool: + def is_product_base_type_supported(self) -> bool: raise NotImplementedError() def get_server_version(self) -> str: diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index 75bc780ff..353d36005 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -87,7 +87,7 @@ def get_products( if not project_name: return - if product_base_types and not self.product_base_type_supported(): + if product_base_types and not self.is_product_base_type_supported(): raise UnsupportedServerVersion( "Product base type is not supported for your server version." ) @@ -413,7 +413,7 @@ def create_product( """ if ( product_base_type is not None - and not self.product_base_type_supported() + and not self.is_product_base_type_supported() ): raise UnsupportedServerVersion( "Product base type is not supported for your server version." @@ -482,7 +482,7 @@ def update_product( """ if ( product_base_type is not None - and not self.product_base_type_supported() + and not self.is_product_base_type_supported() ): raise UnsupportedServerVersion( "Product base type is not supported for your server version." diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index ce8ccc80f..c77b9bf68 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -130,8 +130,8 @@ def project_entity(self) -> ProjectEntity: self.fill_project_from_server() return self._project_entity - def product_base_type_supported(self) -> bool: - return self._connection.product_base_type_supported() + def is_product_base_type_supported(self) -> bool: + return self._connection.is_product_base_type_supported() def get_attributes_for_type( self, entity_type: EntityType @@ -3619,7 +3619,7 @@ def changes(self) -> dict[str, Any]: changes["productType"] = self._product_type if ( - self._entity_hub.product_base_type_supported() + self._entity_hub.is_product_base_type_supported() and self._orig_product_base_type != self._product_base_type ): changes["productBaseType"] = self._product_base_type @@ -3655,7 +3655,7 @@ def to_create_body_data(self) -> dict[str, Any]: } if ( - self._entity_hub.product_base_type_supported() + self._entity_hub.is_product_base_type_supported() and self.product_base_type ): output["productBaseType"] = self.product_base_type diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 8010a239d..eef96033d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -912,7 +912,7 @@ def graphql_allows_traits_in_representations(self) -> bool: ) return self._graphql_allows_traits_in_representations - def product_base_type_supported(self) -> bool: + def is_product_base_type_supported(self) -> bool: """Product base types are available on server.""" if self._product_base_type_supported is None: major, minor, patch, _, _ = self.server_version_tuple From da9c871a626896ffd5948d6b1719cf30b1a270e9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:50:03 +0100 Subject: [PATCH 261/506] add 'productBaseType' only if is supported --- ayon_api/constants.py | 1 - ayon_api/server_api.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 8f4d00ead..21d75e324 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -144,7 +144,6 @@ "folderId", "active", "productType", - "productBaseType", "data", "status", "tags", diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index eef96033d..292cdfdfe 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1815,6 +1815,9 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: elif entity_type == "product": entity_type_defaults = set(DEFAULT_PRODUCT_FIELDS) + maj_v, min_v, patch_v, _, _ = self.server_version_tuple + if self.is_product_base_type_supported(): + entity_type_defaults.add("productBaseType") elif entity_type == "version": entity_type_defaults = set(DEFAULT_VERSION_FIELDS) From dae95dd12be05fd1806d16e46ba643408d89f1cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 14 Nov 2025 11:53:03 +0100 Subject: [PATCH 262/506] raise error if product base type is not supported but is passed in --- ayon_api/operations.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index dd49da7db..3cf8d1cff 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -9,6 +9,7 @@ from typing import Optional, Any, Iterable from ._api import get_server_api_connection +from .exceptions import UnsupportedServerVersion from .utils import create_entity_id, REMOVED_VALUE, NOT_SET if typing.TYPE_CHECKING: @@ -1267,6 +1268,14 @@ def create_product( "folderId": folder_id, } + if ( + product_base_type + and not self._con.is_product_base_type_supported() + ): + raise UnsupportedServerVersion( + "Product base type is not supported for your server version." + ) + for key, value in ( ("attrib", attrib), ("data", data), @@ -1321,6 +1330,14 @@ def update_product( UpdateOperation: Object of update operation. """ + if ( + product_base_type + and not self._con.is_product_base_type_supported() + ): + raise UnsupportedServerVersion( + "Product base type is not supported for your server version." + ) + update_data = { key: value for key, value in ( From cb7038b0a771ee007c7cf2a5cce5603ae714a056 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:47:11 +0100 Subject: [PATCH 263/506] add more information about the enum --- ayon_api/_api_helpers/projects.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 4daefe404..970abfc3b 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -22,6 +22,23 @@ class ProjectFetchType(Enum): + """How a project has to be fetched to get all requested data. + + Some project data can be received only from GraphQl, and some can be + received only with REST. That is based on requested fields. + + There is also a dedicated endpoint to get information about all projects + but returns very limited information about the project. + + Enums: + GraphQl: Requested project data can be received with GraphQl. + REST: Requested project data can be received with /projects/{project}. + RESTList: Requested project data can be received with /projects. + Can be considered as a subset of 'REST'. + GraphQlAndREST: It is necessary to use GraphQl and REST to get all + requested data. + + """ GraphQl = "GraphQl" REST = "REST" RESTList = "RESTList" From 40bc0aa99c14fcc12e3b0bd43ecd42b0f2b76d55 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:10:38 +0100 Subject: [PATCH 264/506] bump version to '1.2.5' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 63dfba578..21e37c4be 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.5-dev" +__version__ = "1.2.5" diff --git a/pyproject.toml b/pyproject.toml index cfecaa78c..9c7b6e542 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.5-dev" +version = "1.2.5" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 4c13bbf8c94e8ca0d2813d02ae8a9d75a01c919a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:11:10 +0100 Subject: [PATCH 265/506] bump version to '1.2.6-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 21e37c4be..4ef1e97fc 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.5" +__version__ = "1.2.6-dev" diff --git a/pyproject.toml b/pyproject.toml index 9c7b6e542..d244b6f38 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.5" +version = "1.2.6-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 2c914befa85cedf034c77b1eea313d04d0bf81c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Nov 2025 14:59:20 +0100 Subject: [PATCH 266/506] add product type fields to graphql fields --- ayon_api/_api_helpers/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index cda8bb399..a909100cf 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -684,7 +684,7 @@ def _get_project_graphql_fields( must_use_graphql = True fields.discard(field) for f_name in DEFAULT_PRODUCT_TYPE_FIELDS: - fields.add(f"{field}.{f_name}") + graphql_fields.add(f"{field}.{f_name}") elif field.startswith("productTypes"): must_use_graphql = True @@ -694,7 +694,7 @@ def _get_project_graphql_fields( must_use_graphql = True fields.discard(field) for f_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS: - fields.add(f"{field}.{f_name}") + graphql_fields.add(f"{field}.{f_name}") elif field.startswith("productBaseTypes"): must_use_graphql = True From 016d4cf0108ef77ebaedd9d9339a11024427bb6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:13:40 +0100 Subject: [PATCH 267/506] bump version to '1.2.6' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 4ef1e97fc..455568651 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.6-dev" +__version__ = "1.2.6" diff --git a/pyproject.toml b/pyproject.toml index d244b6f38..d97a7b135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.6-dev" +version = "1.2.6" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 42bd84783c64389256b6f37e820a6d5d65944901 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 20 Nov 2025 15:15:25 +0100 Subject: [PATCH 268/506] bump version to '1.2.7-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 455568651..6f4e8852f 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.6" +__version__ = "1.2.7-dev" diff --git a/pyproject.toml b/pyproject.toml index d97a7b135..510314436 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.6" +version = "1.2.7-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 31d6427f94279dfe67e888ffdef3dd14471b96e7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:14:49 +0100 Subject: [PATCH 269/506] added attempt to transfer progress --- ayon_api/utils.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index baf271277..29e1d2163 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -796,6 +796,7 @@ class TransferProgress: """Object to store progress of download/upload from/to server.""" def __init__(self): + self._attempt: int = 0 self._started: bool = False self._transfer_done: bool = False self._transferred: int = 0 @@ -850,6 +851,18 @@ def set_started(self): if self._started: raise ValueError("Progress already started") self._started = True + self._attempt = 1 + + def get_attempt(self) -> int: + """Find out which attempt of progress it is.""" + return self._attempt + + def next_attempt(self) -> None: + """Start new attempt of progress.""" + if not self._started: + raise ValueError("Progress did not start yet") + self._attempt += 1 + self._transferred = 0 def get_transfer_done(self) -> bool: """Transfer finished. From 2b2f1946219280cbee002e0d8f2cc8cfb473b1e3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:17:30 +0100 Subject: [PATCH 270/506] fix _endpoint_to_url and use it --- ayon_api/server_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 292cdfdfe..db2251b86 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1329,7 +1329,7 @@ def delete(self, entrypoint: str, **kwargs): def _endpoint_to_url( self, endpoint: str, - use_rest: Optional[bool] = True + use_rest: bool = True, ) -> str: """Cleanup endpoint and return full url to AYON server. @@ -1347,7 +1347,7 @@ def _endpoint_to_url( endpoint = endpoint.lstrip("/").rstrip("/") if endpoint.startswith(self._base_url): return endpoint - base_url = self._rest_url if use_rest else self._graphql_url + base_url = self._rest_url if use_rest else self._base_url return f"{base_url}/{endpoint}" def _download_file_to_stream( @@ -1399,7 +1399,7 @@ def download_file_to_stream( if not chunk_size: chunk_size = self.default_download_chunk_size - url = self._endpoint_to_url(endpoint) + url = self._endpoint_to_url(endpoint, use_rest=False) if progress is None: progress = TransferProgress() @@ -1580,7 +1580,7 @@ def upload_file_from_stream( requests.Response: Response object """ - url = self._endpoint_to_url(endpoint) + url = self._endpoint_to_url(endpoint, use_rest=False) # Create dummy object so the function does not have to check # 'progress' variable everywhere From 57db0a1805bc7a7c4c46b3f11c855371130961fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:18:06 +0100 Subject: [PATCH 271/506] implement download retries --- ayon_api/server_api.py | 44 +++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index db2251b86..42032d2fd 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1351,21 +1351,47 @@ def _endpoint_to_url( return f"{base_url}/{endpoint}" def _download_file_to_stream( - self, url: str, stream, chunk_size, progress + self, + url: str, + stream: StreamType, + chunk_size: int, + progress: TransferProgress, ): - kwargs = {"stream": True} + headers = self.get_headers() + kwargs = { + "stream": True, + "headers": headers, + } if self._session is None: - kwargs["headers"] = self.get_headers() get_func = self._base_functions_mapping[RequestTypes.get] else: get_func = self._session_functions_mapping[RequestTypes.get] - with get_func(url, **kwargs) as response: - response.raise_for_status() - progress.set_content_size(response.headers["Content-length"]) - for chunk in response.iter_content(chunk_size=chunk_size): - stream.write(chunk) - progress.add_transferred_chunk(len(chunk)) + retries = self.get_default_max_retries() + for attempt in range(retries): + # Continue in download + offset = progress.get_transferred_size() + if offset > 0: + headers["Range"] = f"bytes={offset}-" + + try: + with get_func(url, **kwargs) as response: + response.raise_for_status() + progress.set_content_size( + response.headers["Content-length"] + ) + for chunk in response.iter_content(chunk_size=chunk_size): + stream.write(chunk) + progress.add_transferred_chunk(len(chunk)) + break + + except ( + requests.exceptions.Timeout, + requests.exceptions.ConnectionError, + ): + if attempt == retries: + raise + progress.next_attempt() def download_file_to_stream( self, From b642fc3cddc6224bf2ac3d6035e49d771cdfa22d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Dec 2025 18:18:14 +0100 Subject: [PATCH 272/506] implement upload retries --- ayon_api/server_api.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 42032d2fd..ded3cdc3d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1569,11 +1569,26 @@ def _upload_file( if not chunk_size: chunk_size = self.default_upload_chunk_size - response = post_func( - url, - data=self._upload_chunks_iter(stream, progress, chunk_size), - **kwargs - ) + retries = self.get_default_max_retries() + response = None + for attempt in range(retries): + try: + response = post_func( + url, + data=self._upload_chunks_iter( + stream, progress, chunk_size + ), + **kwargs + ) + break + + except ( + requests.exceptions.Timeout, + requests.exceptions.ConnectionError, + ): + if attempt == retries: + raise + progress.next_attempt() response.raise_for_status() return response From eff935084318d2641b1cd89f3e801e79b1b51016 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 11:13:17 +0100 Subject: [PATCH 273/506] add explicit reset of transferred --- ayon_api/server_api.py | 1 + ayon_api/utils.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index ded3cdc3d..e2d8524e3 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1589,6 +1589,7 @@ def _upload_file( if attempt == retries: raise progress.next_attempt() + progress.reset_transferred() response.raise_for_status() return response diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 29e1d2163..49917c7bb 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -862,7 +862,6 @@ def next_attempt(self) -> None: if not self._started: raise ValueError("Progress did not start yet") self._attempt += 1 - self._transferred = 0 def get_transfer_done(self) -> bool: """Transfer finished. @@ -934,6 +933,10 @@ def set_transferred_size(self, transferred: int): """ self._transferred = transferred + def reset_transferred(self) -> None: + """Reset transferred size to initial value.""" + self._transferred = 0 + def add_transferred_chunk(self, chunk_size: int): """Add transferred chunk size in bytes. From e9b7fe12f40e4eed539d11998b7e05f8f0389692 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 12:04:02 +0100 Subject: [PATCH 274/506] don't use base url for upload --- 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 e2d8524e3..e79256bc3 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1622,7 +1622,7 @@ def upload_file_from_stream( requests.Response: Response object """ - url = self._endpoint_to_url(endpoint, use_rest=False) + url = self._endpoint_to_url(endpoint) # Create dummy object so the function does not have to check # 'progress' variable everywhere From 6a498f6e3542546212104c4d3a6f22e4e7f6dc70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:14:54 +0100 Subject: [PATCH 275/506] change how EntityData work --- ayon_api/entity_hub.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index c77b9bf68..86c002e43 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1418,7 +1418,10 @@ class EntityData(dict): """ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self._orig_data = copy.deepcopy(self) + self._orig_data = { + key: copy.deepcopy(value) + for key, value in self.items() + } def get_changes(self) -> dict[str, Any]: """Changes in entity data. @@ -1437,10 +1440,10 @@ def get_changes(self) -> dict[str, Any]: output[key] = None elif key not in self._orig_data: # New value was set - output[key] = self[key] + output[key] = copy.deepcopy(self[key]) elif self[key] != self._orig_data[key]: # Value was changed - output[key] = self[key] + output[key] = copy.deepcopy(self[key]) return output def get_new_entity_value(self) -> dict[str, AttributeValueType]: @@ -1460,7 +1463,21 @@ def get_new_entity_value(self) -> dict[str, AttributeValueType]: def lock(self) -> None: """Lock changes of entity data.""" - self._orig_data = copy.deepcopy(self) + orig_data = {} + for key, value in self.items(): + try: + key = copy.deepcopy(key) + except RecursionError: + print(f"Failed to create copy of key '{key}'!!!") + raise + + try: + orig_data[key] = copy.deepcopy(value) + except RecursionError: + print(f"Failed to create copy of value '{key}'!!!") + raise + + self._orig_data = orig_data class BaseEntity(ABC): From 365cdd4a06aa574c82209f32a40f2599b345edd2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:18:46 +0100 Subject: [PATCH 276/506] raise new errors to actually see the message --- ayon_api/entity_hub.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 86c002e43..9ddf7912d 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1468,14 +1468,18 @@ def lock(self) -> None: try: key = copy.deepcopy(key) except RecursionError: - print(f"Failed to create copy of key '{key}'!!!") - raise + raise RuntimeError( + f"Failed to create copy of key '{key}'" + " because of recursion!!!" + ) try: orig_data[key] = copy.deepcopy(value) except RecursionError: - print(f"Failed to create copy of value '{key}'!!!") - raise + raise RuntimeError( + f"Failed to create copy of value '{key}'" + " because of recursion!!!" + ) self._orig_data = orig_data From 17ef3f8692b902378a1ac59bd5ad10cd5af894b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:18:51 +0100 Subject: [PATCH 277/506] use lock on init --- ayon_api/entity_hub.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 9ddf7912d..7a8041db7 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1418,10 +1418,9 @@ class EntityData(dict): """ def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - self._orig_data = { - key: copy.deepcopy(value) - for key, value in self.items() - } + self._orig_data = {} + # Fill orig data + self.lock() def get_changes(self) -> dict[str, Any]: """Changes in entity data. From a4134a0224ab0bf43be79d59987dec26ac8c1f33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Dec 2025 17:25:21 +0100 Subject: [PATCH 278/506] softer message Co-authored-by: Roy Nieterau --- ayon_api/entity_hub.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 7a8041db7..2707bd7f7 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1469,7 +1469,7 @@ def lock(self) -> None: except RecursionError: raise RuntimeError( f"Failed to create copy of key '{key}'" - " because of recursion!!!" + " because of recursion." ) try: @@ -1477,7 +1477,7 @@ def lock(self) -> None: except RecursionError: raise RuntimeError( f"Failed to create copy of value '{key}'" - " because of recursion!!!" + " because of recursion." ) self._orig_data = orig_data From 50780adf2e5c5fbc2a2c5ab6cf563496dee42acb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:02:23 +0100 Subject: [PATCH 279/506] bump version to '1.2.7' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 6f4e8852f..8e497fdb9 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.7-dev" +__version__ = "1.2.7" diff --git a/pyproject.toml b/pyproject.toml index 510314436..e000e5442 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.7-dev" +version = "1.2.7" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From e782a19379d9dcfec71ddc33874c94702fd790ad Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 19 Dec 2025 16:03:07 +0100 Subject: [PATCH 280/506] bump version to '1.2.8-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 8e497fdb9..af8eb66cb 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.7" +__version__ = "1.2.8-dev" diff --git a/pyproject.toml b/pyproject.toml index e000e5442..30a39cf22 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.7" +version = "1.2.8-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 39680f9f3f2de82c378eac1e0c9d516f524935d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 5 Jan 2026 13:49:26 +0100 Subject: [PATCH 281/506] set content size only once --- ayon_api/server_api.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e79256bc3..06283c60b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1368,6 +1368,7 @@ def _download_file_to_stream( get_func = self._session_functions_mapping[RequestTypes.get] retries = self.get_default_max_retries() + content_size_set = False for attempt in range(retries): # Continue in download offset = progress.get_transferred_size() @@ -1377,9 +1378,12 @@ def _download_file_to_stream( try: with get_func(url, **kwargs) as response: response.raise_for_status() - progress.set_content_size( - response.headers["Content-length"] - ) + if not content_size_set: + content_size_set = True + progress.set_content_size( + response.headers["Content-length"] + ) + for chunk in response.iter_content(chunk_size=chunk_size): stream.write(chunk) progress.add_transferred_chunk(len(chunk)) From 51003d9cb1df50250d333662c95929445643199d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:25:51 +0100 Subject: [PATCH 282/506] added helper methods to download project files --- ayon_api/__init__.py | 4 ++ ayon_api/_api.py | 74 +++++++++++++++++++++++++++++++++++ ayon_api/_api_helpers/base.py | 22 +++++++++++ ayon_api/server_api.py | 70 +++++++++++++++++++++++++++++++++ 4 files changed, 170 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 8d3ef554c..c43bfcdb1 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -70,6 +70,8 @@ delete, download_file_to_stream, download_file, + download_project_file, + download_project_file_to_stream, upload_file_from_stream, upload_file, upload_reviewable, @@ -349,6 +351,8 @@ "delete", "download_file_to_stream", "download_file", + "download_project_file", + "download_project_file_to_stream", "upload_file_from_stream", "upload_file", "upload_reviewable", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index c13198cbb..d92f0af8b 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -992,6 +992,80 @@ def download_file( ) +def download_project_file( + project_name: str, + file_id: str, + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> TransferProgress: + """Download project file to filepath. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + file_id (str): File id. + filepath (str): Path where file will be downloaded. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + TransferProgress: Progress object. + + """ + con = get_server_api_connection() + return con.download_project_file( + project_name=project_name, + file_id=file_id, + filepath=filepath, + chunk_size=chunk_size, + progress=progress, + ) + + +def download_project_file_to_stream( + project_name: str, + file_id: str, + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> TransferProgress: + """Download project file to a stream. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + file_id (str): File id. + stream (StreamType): Stream where output will be stored. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + TransferProgress: Progress object. + + """ + con = get_server_api_connection() + return con.download_project_file_to_stream( + project_name=project_name, + file_id=file_id, + stream=stream, + chunk_size=chunk_size, + progress=progress, + ) + + def upload_file_from_stream( endpoint: str, stream: StreamType, diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index bf209a0a1..f39604ee7 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -108,6 +108,28 @@ def download_file( ) -> TransferProgress: raise NotImplementedError() + def download_project_file( + self, + project_name: str, + file_id: str, + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + raise NotImplementedError() + + def download_project_file_to_stream( + self, + project_name: str, + file_id: str, + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + raise NotImplementedError() + def get_rest_entity_by_id( self, project_name: str, diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e79256bc3..06bfb83d1 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1496,6 +1496,76 @@ def download_file( return progress + def download_project_file( + self, + project_name: str, + file_id: str, + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + """Download project file to filepath. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + file_id (str): File id. + filepath (str): Path where file will be downloaded. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + TransferProgress: Progress object. + + """ + return self.download_file( + f"api/projects/{project_name}/files/{file_id}", + filepath, + chunk_size=chunk_size, + progress=progress, + ) + + def download_project_file_to_stream( + self, + project_name: str, + file_id: str, + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + """Download project file to a stream. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + file_id (str): File id. + stream (StreamType): Stream where output will be stored. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + TransferProgress: Progress object. + + """ + return self.download_file_to_stream( + f"api/projects/{project_name}/files/{file_id}", + stream, + chunk_size=chunk_size, + progress=progress, + ) + @staticmethod def _upload_chunks_iter( file_stream: StreamType, From 4228a32a16ad402eb2701d2db9f4621b87558d57 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 5 Jan 2026 16:26:48 +0100 Subject: [PATCH 283/506] added product_base_types to global api docstring --- ayon_api/_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index c13198cbb..b02ba5b73 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -4932,6 +4932,8 @@ def get_products( Use 'None' if folder is direct child of project. product_types (Optional[Iterable[str]]): Product types used for filtering. + product_base_types (Optional[Iterable[str]]): Product base types + used for filtering. product_name_regex (Optional[str]): Filter products by name regex. product_path_regex (Optional[str]): Filter products by path regex. Path starts with folder path and ends with product name. From d2481e110288890c0995211f4c52e991cdd9d9c6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Jan 2026 09:16:16 +0100 Subject: [PATCH 284/506] remove methods from base --- ayon_api/_api_helpers/base.py | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index f39604ee7..bf209a0a1 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -108,28 +108,6 @@ def download_file( ) -> TransferProgress: raise NotImplementedError() - def download_project_file( - self, - project_name: str, - file_id: str, - filepath: str, - *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, - ) -> TransferProgress: - raise NotImplementedError() - - def download_project_file_to_stream( - self, - project_name: str, - file_id: str, - stream: StreamType, - *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, - ) -> TransferProgress: - raise NotImplementedError() - def get_rest_entity_by_id( self, project_name: str, From 745d356e8cd9d0784f83157488baa870fa37df13 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 6 Jan 2026 10:48:27 +0100 Subject: [PATCH 285/506] use 'get_content_size' --- ayon_api/server_api.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 06283c60b..f74978d5e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1368,7 +1368,6 @@ def _download_file_to_stream( get_func = self._session_functions_mapping[RequestTypes.get] retries = self.get_default_max_retries() - content_size_set = False for attempt in range(retries): # Continue in download offset = progress.get_transferred_size() @@ -1378,8 +1377,7 @@ def _download_file_to_stream( try: with get_func(url, **kwargs) as response: response.raise_for_status() - if not content_size_set: - content_size_set = True + if progress.get_content_size() is None: progress.set_content_size( response.headers["Content-length"] ) From 6f40da8509aff3bc398818f5921a71e6babb0402 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:51:46 +0100 Subject: [PATCH 286/506] fix label issues in entity hub --- ayon_api/entity_hub.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 2707bd7f7..09791d90d 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -1582,6 +1582,8 @@ def __init__( self._tags = copy.deepcopy(tags) self._thumbnail_id = thumbnail_id + if name == label: + label = None self._orig_name = name self._orig_label = label self._orig_status = status From bf31c8568bdec6d428393d48b365ba856dc61bd4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:56:13 +0100 Subject: [PATCH 287/506] list items are not fetched by default --- ayon_api/_api_helpers/lists.py | 18 +++++++++++++++++- ayon_api/constants.py | 4 ---- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index b6bd79265..3d637d05d 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -26,7 +26,15 @@ def get_entity_lists( active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, ) -> Generator[dict[str, Any], None, None]: - """Fetch entity lists from server. + """Fetch entity lists from AYON server. + + Warnings: + You can't get list items for lists with different 'entityType' in + one call. + + Notes: + To get list items, you have to pass 'items' field or + 'items.{sub-fields you want}' to 'fields' argument. Args: project_name (str): Project name where entity lists are. @@ -43,6 +51,14 @@ def get_entity_lists( if fields is None: fields = self.get_default_fields_for_type("entityList") fields = set(fields) + if "items" in fields: + fields.discard("items") + fields |= { + "items.id", + "items.entityId", + "items.entityType", + "items.position", + } if active is not None: fields.add("active") diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 21d75e324..895cff73e 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -259,8 +259,4 @@ "tags", "updatedAt", "updatedBy", - "items.id", - "items.entityId", - "items.entityType", - "items.position", } From 8a4d73c7f6904812356f259af1d5979f12fc20d3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:56:37 +0100 Subject: [PATCH 288/506] fix available attribs for list entities --- ayon_api/server_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 9df64fbb2..80d940086 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1956,6 +1956,8 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: elif entity_type == "entityList": entity_type_defaults = set(DEFAULT_ENTITY_LIST_FIELDS) + # Attributes scope is 'list' + entity_type = "list" else: raise ValueError(f"Unknown entity type \"{entity_type}\"") From 73a4d5e1c075228f2c03cc8a3faa4c50d356c0b8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:56:53 +0100 Subject: [PATCH 289/506] allow attribs fetch for lists by default --- ayon_api/_api_helpers/lists.py | 26 +++++++++++++++++++++++++- ayon_api/constants.py | 1 + 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 3d637d05d..b8dd49d0a 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -50,7 +50,18 @@ def get_entity_lists( """ if fields is None: fields = self.get_default_fields_for_type("entityList") - fields = set(fields) + + # List does not have 'attrib' field but has 'allAttrib' field + # which is json string and contains only values that are set + o_fields = tuple(fields) + fields = set() + requires_attrib = False + for field in o_fields: + if field == "attrib" or field.startswith("attrib."): + requires_attrib = True + field = "allAttrib" + fields.add(field) + if "items" in fields: fields.discard("items") fields |= { @@ -60,6 +71,10 @@ def get_entity_lists( "items.position", } + available_attribs = [] + if requires_attrib: + available_attribs = self.get_attributes_for_type("list") + if active is not None: fields.add("active") @@ -82,6 +97,15 @@ def get_entity_lists( if isinstance(attributes, str): entity_list["attributes"] = json.loads(attributes) + if requires_attrib: + all_attrib = json.loads( + entity_list.get("allAttrib") or "{}" + ) + entity_list["attrib"] = { + attrib_name: all_attrib.get(attrib_name) + for attrib_name in available_attribs + } + self._convert_entity_data(entity_list) yield entity_list diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 895cff73e..e39fc175f 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -247,6 +247,7 @@ DEFAULT_ENTITY_LIST_FIELDS = { "id", "count", + "allAttrib", "attributes", "active", "createdBy", From b03c34f64179dbfb7bd7f27c8778d2f5376a2651 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:57:01 +0100 Subject: [PATCH 290/506] fix endpoint used to create new list --- ayon_api/_api_helpers/lists.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index b8dd49d0a..9c992b2b8 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -210,9 +210,8 @@ def create_entity_list( kwargs[key] = value response = self.post( - f"projects/{project_name}/lists/{list_id}/items", + f"projects/{project_name}/lists", **kwargs - ) response.raise_for_status() return list_id From b6a6527c124eda5b078d5abe6d65a18d484efc93 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:57:16 +0100 Subject: [PATCH 291/506] use correct method to update list items --- ayon_api/_api_helpers/lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 9c992b2b8..df22e3844 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -382,7 +382,7 @@ def update_entity_list_items( mode (EntityListItemMode): Mode of items update. """ - response = self.post( + response = self.patch( f"projects/{project_name}/lists/{list_id}/items", items=items, mode=mode, From 7ed7df2e0951fd96f21d5a0fda79ba6c84b96f84 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:33:26 +0100 Subject: [PATCH 292/506] bump version to 1.2.8 --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index af8eb66cb..6ab0f0b21 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.8-dev" +__version__ = "1.2.8" diff --git a/pyproject.toml b/pyproject.toml index 30a39cf22..052570995 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.8-dev" +version = "1.2.8" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From a1afd483a77af15aaf016e3fe60ab7b7f16a102a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:34:10 +0100 Subject: [PATCH 293/506] bump version to '1.2.9-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 6ab0f0b21..a48719dd6 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.8" +__version__ = "1.2.9-dev" diff --git a/pyproject.toml b/pyproject.toml index 052570995..41351c218 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.8" +version = "1.2.9-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From ed01421f81f19cf899465f66ebd4270613a485d7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:28:43 +0100 Subject: [PATCH 294/506] add typing helpers for filters --- ayon_api/typing.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 008490093..0bcf2d9d4 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -602,3 +602,23 @@ class ActionConfigResponse(TypedDict): class EntityListAttributeDefinitionDict(TypedDict): name: str data: dict[str, Any] + + +AdvancedFilterOperator = Literal["and", "or"] +AdvancedFilterConditionOperator = Literal[ + "eq", "lt", "gt", "lte", "gte", "ne", + "isnull", "notnull", + "in", "notin", "contains", "excludes", + "any", "like" +] + + +class AdvancedFilterConditionDict(TypedDict): + key: str + value: Any + operator: AdvancedFilterConditionOperator + + +class AdvancedFilterDict(TypedDict): + conditions: list[Union[AdvancedFilterConditionDict, "AdvancedFilterDict"]] + operator: AdvancedFilterOperator From babb45c227ed4fb1d25ffce8bd1775f2a13ddbca Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:30:37 +0100 Subject: [PATCH 295/506] added filters to entity arguments --- ayon_api/_api_helpers/base.py | 7 +++++- ayon_api/_api_helpers/folders.py | 14 +++++++----- ayon_api/_api_helpers/products.py | 17 +++++++++------ ayon_api/_api_helpers/representations.py | 24 +++++++++++++-------- ayon_api/_api_helpers/tasks.py | 27 ++++++++++++++++++------ ayon_api/_api_helpers/versions.py | 20 ++++++++++++------ ayon_api/graphql_queries.py | 12 +++++++++++ ayon_api/server_api.py | 10 +++++++++ 8 files changed, 96 insertions(+), 35 deletions(-) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index bf209a0a1..0a3d19550 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -2,7 +2,7 @@ import logging import typing -from typing import Optional, Any, Iterable +from typing import Optional, Any, Iterable, Union import requests @@ -142,6 +142,11 @@ def _prepare_fields( ): raise NotImplementedError() + def _prepare_advanced_filters( + self, filters: Union[str, dict[str, Any], None] + ) -> Optional[str]: + raise NotImplementedError() + def _convert_entity_data(self, entity: AnyEntityDict): raise NotImplementedError() diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index fbef4e485..71535bdd5 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -21,6 +21,7 @@ FolderDict, FlatFolderDict, ProjectHierarchyDict, + AdvancedFilterDict, ) @@ -216,6 +217,7 @@ def get_folders( tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, has_links: Optional[bool] = None, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False ) -> Generator[FolderDict, None, None]: @@ -257,6 +259,7 @@ def get_folders( Both are returned if is set to None. has_links (Optional[Literal[IN, OUT, ANY]]): Filter representations with IN/OUT/ANY links. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -270,11 +273,11 @@ def get_folders( if not project_name: return - filters = { + graphql_filters = { "projectName": project_name } if not prepare_list_filters( - filters, + graphql_filters, ("folderIds", folder_ids), ("folderPaths", folder_paths), ("folderNames", folder_names), @@ -291,9 +294,10 @@ def get_folders( ("folderHasTasks", has_tasks), ("folderHasLinks", has_links), ("folderHasChildren", has_children), + ("filter", self._prepare_advanced_filters(filters)), ): if filter_value is not None: - filters[filter_key] = filter_value + graphql_filters[filter_key] = filter_value if parent_ids is not None: parent_ids = set(parent_ids) @@ -313,7 +317,7 @@ def get_folders( parent_ids.remove(project_name) parent_ids.add("root") - filters["parentFolderIds"] = list(parent_ids) + graphql_filters["parentFolderIds"] = list(parent_ids) if not fields: fields = self.get_default_fields_for_type("folder") @@ -328,7 +332,7 @@ def get_folders( fields.add("ownAttrib") query = folders_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) for parsed_data in query.continuous_query(self): diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index 353d36005..e6fb79c3c 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -18,7 +18,7 @@ from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: - from ayon_api.typing import ProductDict, ProductTypeDict + from ayon_api.typing import ProductDict, ProductTypeDict, AdvancedFilterDict class ProductsAPI(BaseServerAPI): @@ -43,6 +43,7 @@ def get_products( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER ) -> Generator[ProductDict, None, None]: @@ -74,6 +75,7 @@ def get_products( for filtering. active (Optional[bool]): Filter active/inactive products. Both are returned if is set to None. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -145,18 +147,18 @@ def get_products( fields.add("folderId") # Prepare filters for query - filters = { + graphql_filters = { "projectName": project_name } if filter_folder_ids: - filters["folderIds"] = list(filter_folder_ids) + graphql_filters["folderIds"] = list(filter_folder_ids) if filter_product_names: - filters["productNames"] = list(filter_product_names) + graphql_filters["productNames"] = list(filter_product_names) if not prepare_list_filters( - filters, + graphql_filters, ("productIds", product_ids), ("productTypes", product_types), ("productBaseTypes", product_base_types), @@ -168,12 +170,13 @@ def get_products( for filter_key, filter_value in ( ("productNameRegex", product_name_regex), ("productPathRegex", product_path_regex), + ("filter", self._prepare_advanced_filters(filters)), ): if filter_value: - filters[filter_key] = filter_value + graphql_filters[filter_key] = filter_value query = products_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) parsed_data = query.query(self) diff --git a/ayon_api/_api_helpers/representations.py b/ayon_api/_api_helpers/representations.py index 2eb9814cb..39fdf9f64 100644 --- a/ayon_api/_api_helpers/representations.py +++ b/ayon_api/_api_helpers/representations.py @@ -20,7 +20,7 @@ from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: - from ayon_api.typing import RepresentationDict + from ayon_api.typing import RepresentationDict, AdvancedFilterDict class RepresentationsAPI(BaseServerAPI): @@ -42,6 +42,7 @@ def get_representations( tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, has_links: Optional[str] = None, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, ) -> Generator[RepresentationDict, None, None]: @@ -72,6 +73,7 @@ def get_representations( Both are returned when 'None' is passed. has_links (Optional[Literal[IN, OUT, ANY]]): Filter representations with IN/OUT/ANY links. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. @@ -106,7 +108,7 @@ def get_representations( fields.discard("files") fields |= REPRESENTATION_FILES_FIELDS - filters = { + graphql_filters = { "projectName": project_name } @@ -114,7 +116,7 @@ def get_representations( representation_ids = set(representation_ids) if not representation_ids: return - filters["representationIds"] = list(representation_ids) + graphql_filters["representationIds"] = list(representation_ids) version_ids_filter = None representation_names_filter = None @@ -140,29 +142,33 @@ def get_representations( return if version_ids_filter: - filters["versionIds"] = list(version_ids_filter) + graphql_filters["versionIds"] = list(version_ids_filter) if representation_names_filter: - filters["representationNames"] = list(representation_names_filter) + graphql_filters["representationNames"] = list(representation_names_filter) if statuses is not None: statuses = set(statuses) if not statuses: return - filters["representationStatuses"] = list(statuses) + graphql_filters["representationStatuses"] = list(statuses) if tags is not None: tags = set(tags) if not tags: return - filters["representationTags"] = list(tags) + graphql_filters["representationTags"] = list(tags) if has_links is not None: - filters["representationHasLinks"] = has_links.upper() + graphql_filters["representationHasLinks"] = has_links.upper() + + filters = self._prepare_advanced_filters(filters) + if filters: + graphql_filters["filter"] = filters query = representations_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) for parsed_data in query.continuous_query(self): diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index aa984032b..4109d72ed 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -17,7 +17,7 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from ayon_api.typing import TaskDict + from ayon_api.typing import TaskDict, AdvancedFilterDict class TasksAPI(BaseServerAPI): @@ -38,6 +38,7 @@ def get_tasks( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False ) -> Generator[TaskDict, None, None]: @@ -62,6 +63,7 @@ def get_tasks( filtering. active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -75,11 +77,11 @@ def get_tasks( if not project_name: return - filters = { + graphql_filters = { "projectName": project_name } if not prepare_list_filters( - filters, + graphql_filters, ("taskIds", task_ids), ("taskNames", task_names), ("taskTypes", task_types), @@ -91,6 +93,10 @@ def get_tasks( ): return + filters = self._prepare_advanced_filters(filters) + if filters: + graphql_filters["filter"] = filters + if not fields: fields = self.get_default_fields_for_type("task") else: @@ -101,7 +107,7 @@ def get_tasks( fields.add("active") query = tasks_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) for parsed_data in query.continuous_query(self): @@ -193,6 +199,7 @@ def get_tasks_by_folder_paths( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False ) -> dict[str, list[TaskDict]]: @@ -215,6 +222,7 @@ def get_tasks_by_folder_paths( filtering. active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -230,12 +238,13 @@ def get_tasks_by_folder_paths( if not project_name or not folder_paths: return {} - filters = { + graphql_filters = { "projectName": project_name, "folderPaths": list(folder_paths), } + if not prepare_list_filters( - filters, + graphql_filters, ("taskNames", task_names), ("taskTypes", task_types), ("taskAssigneesAny", assignees), @@ -245,6 +254,10 @@ def get_tasks_by_folder_paths( ): return {} + filters = self._prepare_advanced_filters(filters) + if filters: + graphql_filters["filter"] = filters + if not fields: fields = self.get_default_fields_for_type("task") else: @@ -255,7 +268,7 @@ def get_tasks_by_folder_paths( fields.add("active") query = tasks_by_folder_paths_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) output = { diff --git a/ayon_api/_api_helpers/versions.py b/ayon_api/_api_helpers/versions.py index fe8a02469..97800451c 100644 --- a/ayon_api/_api_helpers/versions.py +++ b/ayon_api/_api_helpers/versions.py @@ -15,7 +15,7 @@ from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: - from ayon_api.typing import VersionDict + from ayon_api.typing import VersionDict, AdvancedFilterDict class VersionsAPI(BaseServerAPI): @@ -37,6 +37,7 @@ def get_versions( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER ) -> Generator[VersionDict, None, None]: @@ -63,6 +64,7 @@ def get_versions( for filtering. active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for version. All possible folder fields are returned if 'None' is passed. @@ -98,11 +100,12 @@ def get_versions( if not hero and not standard: return - filters = { + graphql_filters = { "projectName": project_name } + if not prepare_list_filters( - filters, + graphql_filters, ("taskIds", task_ids), ("versionIds", version_ids), ("productIds", product_ids), @@ -113,6 +116,11 @@ def get_versions( ): return + + filters = self._prepare_advanced_filters(filters) + if filters: + graphql_filters["filter"] = filters + queries = [] # Add filters based on 'hero' and 'standard' # NOTE: There is not a filter to "ignore" hero versions or to get @@ -123,14 +131,14 @@ def get_versions( # This query all versions standard + hero # - hero must be filtered out if is not enabled during loop query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) queries.append(query) else: if hero: # Add hero query if hero is enabled hero_query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): hero_query.set_variable_value(attr, filter_value) hero_query.set_variable_value("heroOnly", True) @@ -138,7 +146,7 @@ def get_versions( if standard: standard_query = versions_graphql_query(fields) - for attr, filter_value in filters.items(): + for attr, filter_value in graphql_filters.items(): standard_query.set_variable_value(attr, filter_value) if latest: diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 815cc9a77..df0a1e69e 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -139,6 +139,7 @@ def folders_graphql_query(fields): "folderAssigneesAll", "[String!]" ) tags_var = query.add_variable("folderTags", "[String!]") + filter_var = query.add_variable("filter", "String!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -157,6 +158,7 @@ def folders_graphql_query(fields): folders_field.set_filter("hasTasks", has_tasks_var) folders_field.set_filter("hasLinks", has_links_var) folders_field.set_filter("hasChildren", has_children_var) + folders_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) add_links_fields(folders_field, nested_fields) @@ -188,6 +190,7 @@ def tasks_graphql_query(fields): assignees_all_var = query.add_variable("taskAssigneesAll", "[String!]") statuses_var = query.add_variable("taskStatuses", "[String!]") tags_var = query.add_variable("taskTags", "[String!]") + filter_var = query.add_variable("filter", "String!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -203,6 +206,7 @@ def tasks_graphql_query(fields): tasks_field.set_filter("assignees", assignees_all_var) tasks_field.set_filter("statuses", statuses_var) tasks_field.set_filter("tags", tags_var) + tasks_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) add_links_fields(tasks_field, nested_fields) @@ -233,6 +237,7 @@ def tasks_by_folder_paths_graphql_query(fields): assignees_all_var = query.add_variable("taskAssigneesAll", "[String!]") statuses_var = query.add_variable("taskStatuses", "[String!]") tags_var = query.add_variable("taskTags", "[String!]") + filter_var = query.add_variable("filter", "String!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -250,6 +255,7 @@ def tasks_by_folder_paths_graphql_query(fields): tasks_field.set_filter("assignees", assignees_all_var) tasks_field.set_filter("statuses", statuses_var) tasks_field.set_filter("tags", tags_var) + tasks_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) add_links_fields(tasks_field, nested_fields) @@ -284,6 +290,7 @@ def products_graphql_query(fields): product_path_regex_var = query.add_variable("productPathRegex", "String!") statuses_var = query.add_variable("productStatuses", "[String!]") tags_var = query.add_variable("productTags", "[String!]") + filter_var = query.add_variable("filter", "String!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -298,6 +305,7 @@ def products_graphql_query(fields): products_field.set_filter("tags", tags_var) products_field.set_filter("nameEx", product_name_regex_var) products_field.set_filter("pathEx", product_path_regex_var) + products_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) add_links_fields(products_field, nested_fields) @@ -333,6 +341,7 @@ def versions_graphql_query(fields): ) statuses_var = query.add_variable("versionStatuses", "[String!]") tags_var = query.add_variable("versionTags", "[String!]") + filter_var = query.add_variable("filter", "String!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -347,6 +356,7 @@ def versions_graphql_query(fields): versions_field.set_filter("heroOrLatestOnly", hero_or_latest_only_var) versions_field.set_filter("statuses", statuses_var) versions_field.set_filter("tags", tags_var) + versions_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) add_links_fields(versions_field, nested_fields) @@ -383,6 +393,7 @@ def representations_graphql_query(fields): tags_var = query.add_variable( "representationTags", "[String!]" ) + filter_var = query.add_variable("filter", "String!") project_field = query.add_field("project") project_field.set_filter("name", project_name_var) @@ -394,6 +405,7 @@ def representations_graphql_query(fields): repres_field.set_filter("hasLinks", has_links_var) repres_field.set_filter("statuses", statuses_var) repres_field.set_filter("tags", tags_var) + repres_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) add_links_fields(repres_field, nested_fields) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 80d940086..4dc4b3449 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -2265,6 +2265,16 @@ def _prepare_fields( ) } + def _prepare_advanced_filters( + self, filters: Union[str, dict[str, Any], None] + ) -> Optional[str]: + if not filters: + return None + + if isinstance(filters, dict): + return json.dumps(filters) + return filters + def _convert_entity_data(self, entity: AnyEntityDict): if not entity or "data" not in entity: return From 0ad429c438fc52edaeaa34fead0fbc10e1e3e622 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:31:26 +0100 Subject: [PATCH 296/506] update public api --- ayon_api/_api.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 5681df929..06ece19b0 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -84,6 +84,7 @@ ActionModeType, StreamType, EntityListAttributeDefinitionDict, + AdvancedFilterDict, ) from ._api_helpers.links import CreateLinkData @@ -4218,6 +4219,7 @@ def get_folders( tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, has_links: Optional[bool] = None, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, ) -> Generator[FolderDict, None, None]: @@ -4259,6 +4261,7 @@ def get_folders( Both are returned if is set to None. has_links (Optional[Literal[IN, OUT, ANY]]): Filter representations with IN/OUT/ANY links. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -4286,6 +4289,7 @@ def get_folders( tags=tags, active=active, has_links=has_links, + filters=filters, fields=fields, own_attributes=own_attributes, ) @@ -4571,6 +4575,7 @@ def get_tasks( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, ) -> Generator[TaskDict, None, None]: @@ -4595,6 +4600,7 @@ def get_tasks( filtering. active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -4617,6 +4623,7 @@ def get_tasks( statuses=statuses, tags=tags, active=active, + filters=filters, fields=fields, own_attributes=own_attributes, ) @@ -4695,6 +4702,7 @@ def get_tasks_by_folder_paths( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, ) -> dict[str, list[TaskDict]]: @@ -4717,6 +4725,7 @@ def get_tasks_by_folder_paths( filtering. active (Optional[bool]): Filter active/inactive tasks. Both are returned if is set to None. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -4739,6 +4748,7 @@ def get_tasks_by_folder_paths( statuses=statuses, tags=tags, active=active, + filters=filters, fields=fields, own_attributes=own_attributes, ) @@ -4988,6 +4998,7 @@ def get_products( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, ) -> Generator[ProductDict, None, None]: @@ -5019,6 +5030,7 @@ def get_products( for filtering. active (Optional[bool]): Filter active/inactive products. Both are returned if is set to None. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for folder. All possible folder fields are returned if 'None' is passed. @@ -5043,6 +5055,7 @@ def get_products( statuses=statuses, tags=tags, active=active, + filters=filters, fields=fields, own_attributes=own_attributes, ) @@ -5325,6 +5338,7 @@ def get_versions( statuses: Optional[Iterable[str]] = None, tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, ) -> Generator[VersionDict, None, None]: @@ -5351,6 +5365,7 @@ def get_versions( for filtering. active (Optional[bool]): Receive active/inactive entities. Both are returned when 'None' is passed. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for version. All possible folder fields are returned if 'None' is passed. @@ -5374,6 +5389,7 @@ def get_versions( statuses=statuses, tags=tags, active=active, + filters=filters, fields=fields, own_attributes=own_attributes, ) @@ -5819,6 +5835,7 @@ def get_representations( tags: Optional[Iterable[str]] = None, active: Optional[bool] = True, has_links: Optional[str] = None, + filters: Optional[AdvancedFilterDict] = None, fields: Optional[Iterable[str]] = None, own_attributes=_PLACEHOLDER, ) -> Generator[RepresentationDict, None, None]: @@ -5849,6 +5866,7 @@ def get_representations( Both are returned when 'None' is passed. has_links (Optional[Literal[IN, OUT, ANY]]): Filter representations with IN/OUT/ANY links. + filters (Optional[AdvancedFilterDict]): Advanced filtering options. fields (Optional[Iterable[str]]): Fields to be queried for representation. All possible fields are returned if 'None' is passed. @@ -5871,6 +5889,7 @@ def get_representations( tags=tags, active=active, has_links=has_links, + filters=filters, fields=fields, own_attributes=own_attributes, ) @@ -7282,7 +7301,15 @@ def get_entity_lists( active: Optional[bool] = None, fields: Optional[Iterable[str]] = None, ) -> Generator[dict[str, Any], None, None]: - """Fetch entity lists from server. + """Fetch entity lists from AYON server. + + Warnings: + You can't get list items for lists with different 'entityType' in + one call. + + Notes: + To get list items, you have to pass 'items' field or + 'items.{sub-fields you want}' to 'fields' argument. Args: project_name (str): Project name where entity lists are. From 380dfb5770a5e07625d229e2e6e79faee36ea020 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:51:01 +0100 Subject: [PATCH 297/506] fix formatting --- ayon_api/_api_helpers/products.py | 6 +++++- ayon_api/_api_helpers/representations.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index e6fb79c3c..db1e29a0c 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -18,7 +18,11 @@ from .base import BaseServerAPI, _PLACEHOLDER if typing.TYPE_CHECKING: - from ayon_api.typing import ProductDict, ProductTypeDict, AdvancedFilterDict + from ayon_api.typing import ( + ProductDict, + ProductTypeDict, + AdvancedFilterDict, + ) class ProductsAPI(BaseServerAPI): diff --git a/ayon_api/_api_helpers/representations.py b/ayon_api/_api_helpers/representations.py index 39fdf9f64..56ac8e372 100644 --- a/ayon_api/_api_helpers/representations.py +++ b/ayon_api/_api_helpers/representations.py @@ -145,7 +145,9 @@ def get_representations( graphql_filters["versionIds"] = list(version_ids_filter) if representation_names_filter: - graphql_filters["representationNames"] = list(representation_names_filter) + graphql_filters["representationNames"] = list( + representation_names_filter + ) if statuses is not None: statuses = set(statuses) From 03746595f4dc8c717a1d4d5ece0bef081282a66a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:08:40 +0100 Subject: [PATCH 298/506] bump version to '1.2.9' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index a48719dd6..001d627ca 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.9-dev" +__version__ = "1.2.9" diff --git a/pyproject.toml b/pyproject.toml index 41351c218..d8fb17122 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.9-dev" +version = "1.2.9" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 059c7755bf604cc01de6f8b70fc82181a3fe4c03 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:09:15 +0100 Subject: [PATCH 299/506] bump version to '1.2.10-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 001d627ca..3cda4fdab 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.9" +__version__ = "1.2.10-dev" diff --git a/pyproject.toml b/pyproject.toml index d8fb17122..099e1e5e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.9" +version = "1.2.10-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 671b10aa6656ad3579313760f9cf20241859e8d6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 20 Jan 2026 12:32:11 +0100 Subject: [PATCH 300/506] allow to add 'data' to a link --- ayon_api/_api_helpers/links.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index 6c23b504c..dc73f7890 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -203,6 +203,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, + data: Optional[dict[str, Any]] = None, ) -> CreateLinkData: """Create link between 2 entities. @@ -222,7 +223,8 @@ def create_link( output_id (str): Output entity id. output_type (str): Entity type of output entity. link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + data (Optional[dict[str, Any]]): Additional data to be stored + with the link. Returns: CreateLinkData: Information about link. @@ -242,6 +244,9 @@ def create_link( if link_name: kwargs["name"] = link_name + if data: + kwargs["data"] = data + response = self.post( f"projects/{project_name}/links", **kwargs ) From 51652bb79c3c025a4b0f9929ceb322f0b92314ed Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Feb 2026 23:00:44 +0100 Subject: [PATCH 301/506] Include detail message upon HTTPError responses --- ayon_api/utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 49917c7bb..b582d79df 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -178,6 +178,17 @@ def raise_for_status(self, message=None): except requests.exceptions.HTTPError as exc: if message is None: message = str(exc) + + # Get 'detail' from response.json() if possible because it'll be + # more descriptive than default http error message + try: + detail = exc.response.json()["detail"] + except (AttributeError, KeyError): + pass + else: + if detail: + message = f"{message}\n\tDetail: {detail}" + raise HTTPRequestError(message, exc.response) def __enter__(self, *args, **kwargs): From e4563b9173cdce133b6e9fb100aca7c7778bb869 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Feb 2026 23:54:32 +0100 Subject: [PATCH 302/506] Fix detail message formatting --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index b582d79df..13d303e1b 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -187,7 +187,7 @@ def raise_for_status(self, message=None): pass else: if detail: - message = f"{message}\n\tDetail: {detail}" + message = f"{message} ({detail})" raise HTTPRequestError(message, exc.response) From 0fd1349e04a3cc655a15aad9a9c98cbea3a4752f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 4 Feb 2026 23:56:00 +0100 Subject: [PATCH 303/506] Pass RequestsJSONDecodeError --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 13d303e1b..c2303f504 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -183,7 +183,7 @@ def raise_for_status(self, message=None): # more descriptive than default http error message try: detail = exc.response.json()["detail"] - except (AttributeError, KeyError): + except (AttributeError, KeyError, RequestsJSONDecodeError): pass else: if detail: From 90ac8011aba7ac72ed5bce89d6d876cdfe5a4a3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Feb 2026 09:43:06 +0100 Subject: [PATCH 304/506] move detail logic to try block --- ayon_api/utils.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index c2303f504..1e6837e90 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -183,11 +183,10 @@ def raise_for_status(self, message=None): # more descriptive than default http error message try: detail = exc.response.json()["detail"] - except (AttributeError, KeyError, RequestsJSONDecodeError): - pass - else: if detail: message = f"{message} ({detail})" + except (AttributeError, KeyError, RequestsJSONDecodeError): + pass raise HTTPRequestError(message, exc.response) From b0c1d394f2eb296bc07acfef0aeb92603d945c47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:23:43 +0100 Subject: [PATCH 305/506] debug log response data on fail --- ayon_api/utils.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 1e6837e90..383d5932b 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -4,6 +4,8 @@ import re import datetime import copy +import logging +import json import uuid import string import platform @@ -101,8 +103,9 @@ def _get_description(response): return HTTPStatus(response.status).description -class RestApiResponse(object): +class RestApiResponse: """API Response.""" + log = logging.getLogger("RestApiResponse") def __init__(self, response, data=None): if response is None: @@ -179,15 +182,20 @@ def raise_for_status(self, message=None): if message is None: message = str(exc) - # Get 'detail' from response.json() if possible because it'll be - # more descriptive than default http error message + data = {} try: - detail = exc.response.json()["detail"] - if detail: - message = f"{message} ({detail})" + data = self.data or data except (AttributeError, KeyError, RequestsJSONDecodeError): pass + detail = data.get("detail") + if detail: + message = f"{message} ({detail})" + self.log.debug( + "HTTP request error: %s\n%s", + message, + json.dumps(data, indent=4), + ) raise HTTPRequestError(message, exc.response) def __enter__(self, *args, **kwargs): From da01ec223f0af626545e45064abd18619c323bc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Feb 2026 10:24:58 +0100 Subject: [PATCH 306/506] change level to warning --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 383d5932b..1c6c1c5cb 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -191,7 +191,7 @@ def raise_for_status(self, message=None): detail = data.get("detail") if detail: message = f"{message} ({detail})" - self.log.debug( + self.log.warning( "HTTP request error: %s\n%s", message, json.dumps(data, indent=4), From 8366508a11e052c84223a123209bb9eafc344c0e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 5 Feb 2026 15:04:46 +0100 Subject: [PATCH 307/506] don't log data if are not available --- ayon_api/utils.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 1c6c1c5cb..2649ab2a3 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -188,14 +188,20 @@ def raise_for_status(self, message=None): except (AttributeError, KeyError, RequestsJSONDecodeError): pass - detail = data.get("detail") - if detail: - message = f"{message} ({detail})" + submsg = "" + if data: + submsg = json.dumps(data, indent=4) + self.log.warning( - "HTTP request error: %s\n%s", + "HTTP request error: %s%s%s", message, - json.dumps(data, indent=4), + "\n" if submsg else "", + submsg, ) + + detail = data.get("detail") + if detail: + message = f"{message} ({detail})" raise HTTPRequestError(message, exc.response) def __enter__(self, *args, **kwargs): From b6d3d3f2f88a9ffbef5ea52aae14ce38dd80b863 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Feb 2026 09:49:36 +0100 Subject: [PATCH 308/506] remove try block --- ayon_api/utils.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 2649ab2a3..c7d87d2d7 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -182,14 +182,8 @@ def raise_for_status(self, message=None): if message is None: message = str(exc) - data = {} - try: - data = self.data or data - except (AttributeError, KeyError, RequestsJSONDecodeError): - pass - submsg = "" - if data: + if self.data: submsg = json.dumps(data, indent=4) self.log.warning( @@ -199,7 +193,7 @@ def raise_for_status(self, message=None): submsg, ) - detail = data.get("detail") + detail = self.data.get("detail") if detail: message = f"{message} ({detail})" raise HTTPRequestError(message, exc.response) From aaae9a3cab8f4468d8eb1a2d32bc232163aaaa21 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:08:20 +0100 Subject: [PATCH 309/506] fix variable usage --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index c7d87d2d7..240fc72ac 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -184,7 +184,7 @@ def raise_for_status(self, message=None): submsg = "" if self.data: - submsg = json.dumps(data, indent=4) + submsg = json.dumps(self.data, indent=4) self.log.warning( "HTTP request error: %s%s%s", From c9378f5404cdac97335f4f9520ecd098bc7be362 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 6 Feb 2026 10:09:16 +0100 Subject: [PATCH 310/506] move AttributeError to 'data' property --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 240fc72ac..7554c60fa 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -137,7 +137,7 @@ def data(self): if self._data is None: try: self._data = self.orig_response.json() - except RequestsJSONDecodeError: + except (AttributeError, RequestsJSONDecodeError): self._data = {} return self._data From 3fdfcb1ebc57d54c58d8949139cab0a4a07d635e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:24:36 +0100 Subject: [PATCH 311/506] bump version to '1.2.10' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 3cda4fdab..fb3aa5b63 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.10-dev" +__version__ = "1.2.10" diff --git a/pyproject.toml b/pyproject.toml index 099e1e5e9..cb1d4f582 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.10-dev" +version = "1.2.10" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 7f9183cb01714102502ad7dd1cf3973876a81099 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 12 Feb 2026 17:25:13 +0100 Subject: [PATCH 312/506] bump version to '1.2.11-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index fb3aa5b63..0cf289517 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.10" +__version__ = "1.2.11-dev" diff --git a/pyproject.toml b/pyproject.toml index cb1d4f582..94372226a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.10" +version = "1.2.11-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 7be1b3387428b366857c7ab6360770e1c478e9ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 10:15:24 +0100 Subject: [PATCH 313/506] fix routes to download and download things --- ayon_api/_api_helpers/bundles_addons.py | 8 +++++--- ayon_api/_api_helpers/dependency_packages.py | 4 ++-- ayon_api/_api_helpers/installers.py | 4 ++-- ayon_api/_api_helpers/thumbnails.py | 8 ++++---- ayon_api/server_api.py | 2 +- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index 6355b62eb..9fc5de162 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -398,7 +398,7 @@ def upload_addon_zip( """ response = self.upload_file( - "addons/install", + "api/addons/install", src_filepath, progress=progress, request_type=RequestTypes.post, @@ -448,9 +448,11 @@ def download_addon_private_file( "private", filename ) - url = f"{self.get_base_url()}/{endpoint}" self.download_file( - url, dst_filepath, chunk_size=chunk_size, progress=progress + endpoint, + dst_filepath, + chunk_size=chunk_size, + progress=progress, ) return dst_filepath diff --git a/ayon_api/_api_helpers/dependency_packages.py b/ayon_api/_api_helpers/dependency_packages.py index dc1f43e94..a2c72f40a 100644 --- a/ayon_api/_api_helpers/dependency_packages.py +++ b/ayon_api/_api_helpers/dependency_packages.py @@ -189,7 +189,7 @@ def download_dependency_package( route = self._get_dependency_package_route(src_filename) package_filepath = os.path.join(dst_directory, dst_filename) self.download_file( - route, + f"api/{route}", package_filepath, chunk_size=chunk_size, progress=progress @@ -225,7 +225,7 @@ def upload_dependency_package( ) route = self._get_dependency_package_route(dst_filename) - self.upload_file(route, src_filepath, progress=progress) + self.upload_file(f"api/{route}", src_filepath, progress=progress) def _get_dependency_package_route( self, filename: Optional[str] = None diff --git a/ayon_api/_api_helpers/installers.py b/ayon_api/_api_helpers/installers.py index be2bcaaec..72aa5193c 100644 --- a/ayon_api/_api_helpers/installers.py +++ b/ayon_api/_api_helpers/installers.py @@ -143,7 +143,7 @@ def download_installer( """ return self.download_file( - f"desktop/installers/{filename}", + f"api/desktop/installers/{filename}", dst_filepath, chunk_size=chunk_size, progress=progress @@ -168,7 +168,7 @@ def upload_installer( """ return self.upload_file( - f"desktop/installers/{dst_filename}", + f"api/desktop/installers/{dst_filename}", src_filepath, progress=progress ) diff --git a/ayon_api/_api_helpers/thumbnails.py b/ayon_api/_api_helpers/thumbnails.py index 577536e28..9d534fd5e 100644 --- a/ayon_api/_api_helpers/thumbnails.py +++ b/ayon_api/_api_helpers/thumbnails.py @@ -256,7 +256,7 @@ def create_thumbnail( mime_type = get_media_mime_type(src_filepath) response = self.upload_file( - f"projects/{project_name}/thumbnails", + f"api/projects/{project_name}/thumbnails", src_filepath, request_type=RequestTypes.post, headers={"Content-Type": mime_type}, @@ -295,7 +295,7 @@ def create_thumbnail_with_stream( mime_type = get_media_mime_type_for_stream(stream) response = self.upload_file_from_stream( - f"projects/{project_name}/thumbnails", + f"api/projects/{project_name}/thumbnails", stream, request_type=RequestTypes.post, headers={"Content-Type": mime_type}, @@ -325,7 +325,7 @@ def update_thumbnail( mime_type = get_media_mime_type(src_filepath) response = self.upload_file( - f"projects/{project_name}/thumbnails/{thumbnail_id}", + f"api/projects/{project_name}/thumbnails/{thumbnail_id}", src_filepath, request_type=RequestTypes.put, headers={"Content-Type": mime_type}, @@ -351,7 +351,7 @@ def update_thumbnail_from_stream( """ mime_type = get_media_mime_type_for_stream(stream) response = self.upload_file_from_stream( - f"projects/{project_name}/thumbnails/{thumbnail_id}", + f"api/projects/{project_name}/thumbnails/{thumbnail_id}", stream, request_type=RequestTypes.put, headers={"Content-Type": mime_type}, diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 4dc4b3449..d91e16156 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1813,7 +1813,7 @@ def upload_reviewable( query = prepare_query_string({"label": label or None}) endpoint = ( - f"/projects/{project_name}" + f"api/projects/{project_name}" f"/versions/{version_id}/reviewables{query}" ) return self.upload_file( From a524c2b249f441f5dd133af071c34dc9ee169ef5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:13:05 +0100 Subject: [PATCH 314/506] set content size only once --- ayon_api/server_api.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d91e16156..48c0684a1 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1585,13 +1585,7 @@ def _upload_chunks_iter( bytes: Chunk of file. """ - # Get size of file - file_stream.seek(0, io.SEEK_END) - size = file_stream.tell() file_stream.seek(0) - # Set content size to progress object - progress.set_content_size(size) - while True: chunk = file_stream.read(chunk_size) if not chunk: @@ -1643,6 +1637,12 @@ def _upload_file( retries = self.get_default_max_retries() response = None + + # Get size of file + stream.seek(0, io.SEEK_END) + size = stream.tell() + # Set content size to progress object + progress.set_content_size(size) for attempt in range(retries): try: response = post_func( From ee4b45b282e1f303a0ffe025b3eea33c1825b85f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:14:04 +0100 Subject: [PATCH 315/506] don't use rest by default --- ayon_api/server_api.py | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 48c0684a1..5cae48bcd 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1595,17 +1595,18 @@ def _upload_chunks_iter( def _upload_file( self, - url: str, + endpoint: str, stream: StreamType, progress: TransferProgress, request_type: Optional[RequestType] = None, chunk_size: Optional[int] = None, + use_rest: bool = False, **kwargs ) -> requests.Response: """Upload file to server. Args: - url (str): Url where file will be uploaded. + endpoint (str): Endpoint used to upload. stream (StreamType): File stream. progress (TransferProgress): Object that gives ability to track progress. @@ -1623,6 +1624,11 @@ def _upload_file( if request_type is None: request_type = RequestTypes.put + endpoint = endpoint.lstrip("/") + url = self._endpoint_to_url(endpoint, use_rest=use_rest) + + progress.set_destination_url(url) + if self._session is None: headers = kwargs.setdefault("headers", {}) for key, value in self.get_headers().items(): @@ -1672,6 +1678,7 @@ def upload_file_from_stream( stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, + use_rest: bool = False, **kwargs ) -> requests.Response: """Upload file to server from bytes. @@ -1687,6 +1694,8 @@ def upload_file_from_stream( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + use_rest (bool): Use rest api endpoint (prefix + endpoint with 'api/'). **kwargs (Any): Additional arguments that will be passed to request function. @@ -1694,19 +1703,21 @@ def upload_file_from_stream( requests.Response: Response object """ - url = self._endpoint_to_url(endpoint) - # Create dummy object so the function does not have to check # 'progress' variable everywhere if progress is None: progress = TransferProgress() - progress.set_destination_url(url) progress.set_started() try: return self._upload_file( - url, stream, progress, request_type, **kwargs + endpoint, + stream, + progress, + request_type, + use_rest=use_rest, + **kwargs ) except Exception as exc: @@ -1722,6 +1733,7 @@ def upload_file( filepath: str, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, + use_rest: bool = False, **kwargs ) -> requests.Response: """Upload file to server. @@ -1737,6 +1749,8 @@ def upload_file( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + use_rest (bool): Use rest api endpoint (prefix + endpoint with 'api/'). **kwargs (Any): Additional arguments that will be passed to request function. @@ -1751,7 +1765,12 @@ def upload_file( with open(filepath, "rb") as stream: return self.upload_file_from_stream( - endpoint, stream, progress, request_type, **kwargs + endpoint, + stream, + progress, + request_type, + use_rest=use_rest, + **kwargs ) def upload_reviewable( From b4c7c1ce26957b4960c8242cc6bb51c25abfa678 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:17:14 +0100 Subject: [PATCH 316/506] auto-fix endpoint --- ayon_api/server_api.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 5cae48bcd..16ee138d2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1600,7 +1600,6 @@ def _upload_file( progress: TransferProgress, request_type: Optional[RequestType] = None, chunk_size: Optional[int] = None, - use_rest: bool = False, **kwargs ) -> requests.Response: """Upload file to server. @@ -1625,8 +1624,7 @@ def _upload_file( request_type = RequestTypes.put endpoint = endpoint.lstrip("/") - url = self._endpoint_to_url(endpoint, use_rest=use_rest) - + url = self._endpoint_to_url(endpoint, use_rest=False) progress.set_destination_url(url) if self._session is None: @@ -1649,6 +1647,8 @@ def _upload_file( size = stream.tell() # Set content size to progress object progress.set_content_size(size) + + api_prepended = False for attempt in range(retries): try: response = post_func( @@ -1658,6 +1658,13 @@ def _upload_file( ), **kwargs ) + # Auto-fix missing 'api/' + if response.status_code == 405 and not api_prepended: + api_prepended = True + if not endpoint.startswith("api/"): + url = self._endpoint_to_url(endpoint, use_rest=True) + progress.set_destination_url(url) + continue break except ( @@ -1670,6 +1677,11 @@ def _upload_file( progress.reset_transferred() response.raise_for_status() + if api_prepended: + self.log.warning( + f"Auto-fixed endpoint '{endpoint}' -> 'api/{endpoint}'." + " Please fix the endpoit passed to the function." + ) return response def upload_file_from_stream( @@ -1678,7 +1690,6 @@ def upload_file_from_stream( stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, - use_rest: bool = False, **kwargs ) -> requests.Response: """Upload file to server from bytes. @@ -1694,8 +1705,6 @@ def upload_file_from_stream( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. - use_rest (bool): Use rest api endpoint (prefix - endpoint with 'api/'). **kwargs (Any): Additional arguments that will be passed to request function. @@ -1716,7 +1725,6 @@ def upload_file_from_stream( stream, progress, request_type, - use_rest=use_rest, **kwargs ) @@ -1733,7 +1741,6 @@ def upload_file( filepath: str, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, - use_rest: bool = False, **kwargs ) -> requests.Response: """Upload file to server. @@ -1749,8 +1756,6 @@ def upload_file( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. - use_rest (bool): Use rest api endpoint (prefix - endpoint with 'api/'). **kwargs (Any): Additional arguments that will be passed to request function. @@ -1769,7 +1774,6 @@ def upload_file( stream, progress, request_type, - use_rest=use_rest, **kwargs ) From 4c7d27f85513574ec6d888dfe783f4d0b5918c3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:19:08 +0100 Subject: [PATCH 317/506] better check --- ayon_api/server_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 16ee138d2..f516301a2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1661,7 +1661,10 @@ def _upload_file( # Auto-fix missing 'api/' if response.status_code == 405 and not api_prepended: api_prepended = True - if not endpoint.startswith("api/"): + if ( + not endpoint.startswith(self._base_url) + and not endpoint.startswith("api/") + ): url = self._endpoint_to_url(endpoint, use_rest=True) progress.set_destination_url(url) continue From e11c77ca1d5c94b730873486d24ffd5cf908ca6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:31:30 +0100 Subject: [PATCH 318/506] auto-fix download too --- ayon_api/server_api.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f516301a2..aeb637d64 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1352,7 +1352,7 @@ def _endpoint_to_url( def _download_file_to_stream( self, - url: str, + endpoint: str, stream: StreamType, chunk_size: int, progress: TransferProgress, @@ -1367,7 +1367,11 @@ def _download_file_to_stream( else: get_func = self._session_functions_mapping[RequestTypes.get] + url = self._endpoint_to_url(endpoint, use_rest=False) + progress.set_source_url(url) + retries = self.get_default_max_retries() + api_prepended = False for attempt in range(retries): # Continue in download offset = progress.get_transferred_size() @@ -1376,6 +1380,18 @@ def _download_file_to_stream( try: with get_func(url, **kwargs) as response: + # Auto-fix missing 'api/' + if response.status_code == 405 and not api_prepended: + api_prepended = True + if ( + not endpoint.startswith(self._base_url) + and not endpoint.startswith("api/") + ): + url = self._endpoint_to_url( + endpoint, use_rest=True + ) + progress.set_destination_url(url) + continue response.raise_for_status() if progress.get_content_size() is None: progress.set_content_size( @@ -1427,17 +1443,14 @@ def download_file_to_stream( if not chunk_size: chunk_size = self.default_download_chunk_size - url = self._endpoint_to_url(endpoint, use_rest=False) - if progress is None: progress = TransferProgress() - progress.set_source_url(url) progress.set_started() try: self._download_file_to_stream( - url, stream, chunk_size, progress + endpoint, stream, chunk_size, progress ) except Exception as exc: @@ -1648,7 +1661,6 @@ def _upload_file( # Set content size to progress object progress.set_content_size(size) - api_prepended = False for attempt in range(retries): try: response = post_func( From 1fbeb0f7be702fc88db0337519930ef206ffda6e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:32:23 +0100 Subject: [PATCH 319/506] added autofix warning --- ayon_api/server_api.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index aeb637d64..c9a98fc5d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1411,6 +1411,12 @@ def _download_file_to_stream( raise progress.next_attempt() + if api_prepended: + self.log.warning( + f"Auto-fixed endpoint '{endpoint}' -> 'api/{endpoint}'." + " Please fix the endpoit passed to the function." + ) + def download_file_to_stream( self, endpoint: str, From 425e5fd79c0a399ee089a3d4ca0e91207d4feb8e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 11:39:39 +0100 Subject: [PATCH 320/506] add missing variable --- ayon_api/server_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index c9a98fc5d..6705126b2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1667,6 +1667,7 @@ def _upload_file( # Set content size to progress object progress.set_content_size(size) + api_prepended = False for attempt in range(retries): try: response = post_func( From fc27f6f13ef63a80d2d8dd50b5faf5c17595ef47 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:45:54 +0100 Subject: [PATCH 321/506] bump version to '1.2.11' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 0cf289517..0f9694493 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.11-dev" +__version__ = "1.2.11" diff --git a/pyproject.toml b/pyproject.toml index 94372226a..10b8a03a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.11-dev" +version = "1.2.11" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 9095c03b9bdf1a17abd60b60a26e6c520961d9cc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:46:25 +0100 Subject: [PATCH 322/506] bump version to '1.2.12-dev' --- ayon_api/version.py | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 0f9694493..bd13d6f49 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.11" +__version__ = "1.2.12-dev" diff --git a/pyproject.toml b/pyproject.toml index 10b8a03a0..92183a85d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.11" +version = "1.2.12-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} From 1ddb3b492bb8e93e773d00daa19b7e1f036a458d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 23 Feb 2026 21:06:45 +0100 Subject: [PATCH 323/506] Fix log message typo --- ayon_api/server_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6705126b2..65b6faa15 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1414,7 +1414,7 @@ def _download_file_to_stream( if api_prepended: self.log.warning( f"Auto-fixed endpoint '{endpoint}' -> 'api/{endpoint}'." - " Please fix the endpoit passed to the function." + " Please fix the endpoint passed to the function." ) def download_file_to_stream( @@ -1702,7 +1702,7 @@ def _upload_file( if api_prepended: self.log.warning( f"Auto-fixed endpoint '{endpoint}' -> 'api/{endpoint}'." - " Please fix the endpoit passed to the function." + " Please fix the endpoint passed to the function." ) return response From 4aadc95d59efef52610cffeb16d55d68bc75ecd7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:47:04 +0100 Subject: [PATCH 324/506] added helper upload methods for project files --- ayon_api/server_api.py | 68 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 65b6faa15..03086e050 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1517,6 +1517,74 @@ def download_file( return progress + def upload_project_file( + self, + project_name: str, + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> requests.Response: + """Upload project file from a filepath. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + filepath (str): Path where file will be downloaded. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + requests.Response: Requests response. + + """ + return self.upload_file( + f"api/projects/{project_name}/files", + filepath, + chunk_size=chunk_size, + progress=progress, + request_type=RequestTypes.post, + ) + + def upload_project_file_from_stream( + self, + project_name: str, + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> requests.Response: + """Upload project file from a filepath. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + stream (StreamType): Stream used as source for upload. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + requests.Response: Requests response. + + """ + return self.upload_file_from_stream( + f"api/projects/{project_name}/files", + stream, + chunk_size=chunk_size, + progress=progress, + request_type=RequestTypes.post, + ) + def download_project_file( self, project_name: str, From a090ba4855c3b5234745aaaef393cb73ed9a878e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:47:30 +0100 Subject: [PATCH 325/506] add new functions to public api --- ayon_api/__init__.py | 4 +++ ayon_api/_api.py | 68 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index c43bfcdb1..ae73d2090 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -70,6 +70,8 @@ delete, download_file_to_stream, download_file, + upload_project_file, + upload_project_file_from_stream, download_project_file, download_project_file_to_stream, upload_file_from_stream, @@ -351,6 +353,8 @@ "delete", "download_file_to_stream", "download_file", + "upload_project_file", + "upload_project_file_from_stream", "download_project_file", "download_project_file_to_stream", "upload_file_from_stream", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 06ece19b0..6c1ba45bc 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -993,6 +993,74 @@ def download_file( ) +def upload_project_file( + project_name: str, + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> requests.Response: + """Upload project file from a filepath. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + filepath (str): Path where file will be downloaded. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + requests.Response: Requests response. + + """ + con = get_server_api_connection() + return con.upload_project_file( + project_name=project_name, + filepath=filepath, + chunk_size=chunk_size, + progress=progress, + ) + + +def upload_project_file_from_stream( + project_name: str, + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> requests.Response: + """Upload project file from a filepath. + + Project files are usually binary files, such as images, videos, + or other media files that can be accessed via api endpoint + '{server url}/api/projects/{project_name}/files/{file_id}'. + + Args: + project_name (str): Project name. + stream (StreamType): Stream used as source for upload. + chunk_size (Optional[int]): Size of chunks that are received + in single loop. + progress (Optional[TransferProgress]): Object that gives ability + to track download progress. + + Returns: + requests.Response: Requests response. + + """ + con = get_server_api_connection() + return con.upload_project_file_from_stream( + project_name=project_name, + stream=stream, + chunk_size=chunk_size, + progress=progress, + ) + + def download_project_file( project_name: str, file_id: str, From 7be82332edbd55242342e04a270c0b5ebb0068aa Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:48:36 +0100 Subject: [PATCH 326/506] add data to public function for create link --- ayon_api/_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 06ece19b0..5b66a327f 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -6887,6 +6887,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, + data: Optional[dict[str, Any]] = None, ) -> CreateLinkData: """Create link between 2 entities. @@ -6906,7 +6907,8 @@ def create_link( output_id (str): Output entity id. output_type (str): Entity type of output entity. link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + data (Optional[dict[str, Any]]): Additional data to be stored + with the link. Returns: CreateLinkData: Information about link. @@ -6924,6 +6926,7 @@ def create_link( output_id=output_id, output_type=output_type, link_name=link_name, + data=data, ) From 6ba1c9bc775bd9c914850a0918b0bd94212e891b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:51:22 +0100 Subject: [PATCH 327/506] allow to pass project bundle name to bundle methods --- ayon_api/_api.py | 19 ++++++++++++++++++- ayon_api/_api_helpers/bundles_addons.py | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 06ece19b0..08b99e59b 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -2889,6 +2889,7 @@ def get_addon_site_settings( def get_bundle_settings( bundle_name: Optional[str] = None, project_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, @@ -2928,6 +2929,7 @@ def get_bundle_settings( return con.get_bundle_settings( bundle_name=bundle_name, project_name=project_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site, @@ -2936,6 +2938,7 @@ def get_bundle_settings( def get_addons_studio_settings( bundle_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, @@ -2951,6 +2954,8 @@ def get_addons_studio_settings( Args: bundle_name (Optional[str]): Name of bundle for which should be settings received. + project_bundle_name (Optional[str]): Project bundle name for + which should be settings received. variant (Optional[Literal['production', 'staging']]): Name of settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Site id for which want to receive @@ -2968,6 +2973,7 @@ def get_addons_studio_settings( con = get_server_api_connection() return con.get_addons_studio_settings( bundle_name=bundle_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site, @@ -2978,6 +2984,7 @@ def get_addons_studio_settings( def get_addons_project_settings( project_name: str, bundle_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, @@ -3009,6 +3016,8 @@ def get_addons_project_settings( received. bundle_name (Optional[str]): Name of bundle for which should be settings received. + project_bundle_name (Optional[str]): Project bundle name for which + should be settings received. variant (Optional[Literal['production', 'staging']]): Name of settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Site id for which want to receive @@ -3028,6 +3037,7 @@ def get_addons_project_settings( return con.get_addons_project_settings( project_name=project_name, bundle_name=bundle_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site, @@ -3037,6 +3047,7 @@ def get_addons_project_settings( def get_addons_settings( bundle_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, project_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, @@ -3056,6 +3067,8 @@ def get_addons_settings( Args: bundle_name (Optional[str]): Name of bundle for which should be settings received. + project_bundle_name (Optional[str]): Name of project bundle + for which should be settings received. project_name (Optional[str]): Name of project for which should be settings received. variant (Optional[Literal['production', 'staging']]): Name of @@ -3072,6 +3085,7 @@ def get_addons_settings( con = get_server_api_connection() return con.get_addons_settings( bundle_name=bundle_name, + project_bundle_name=project_bundle_name, project_name=project_name, variant=variant, site_id=site_id, @@ -6887,6 +6901,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, + data: Optional[dict[str, Any]] = None, ) -> CreateLinkData: """Create link between 2 entities. @@ -6906,7 +6921,8 @@ def create_link( output_id (str): Output entity id. output_type (str): Entity type of output entity. link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + data (Optional[dict[str, Any]]): Additional data to be stored + with the link. Returns: CreateLinkData: Information about link. @@ -6924,6 +6940,7 @@ def create_link( output_id=output_id, output_type=output_type, link_name=link_name, + data=data, ) diff --git a/ayon_api/_api_helpers/bundles_addons.py b/ayon_api/_api_helpers/bundles_addons.py index 9fc5de162..9bb7bc214 100644 --- a/ayon_api/_api_helpers/bundles_addons.py +++ b/ayon_api/_api_helpers/bundles_addons.py @@ -671,6 +671,7 @@ def get_bundle_settings( self, bundle_name: Optional[str] = None, project_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, @@ -714,6 +715,7 @@ def get_bundle_settings( query = prepare_query_string({ "project_name": project_name or None, "bundle_name": bundle_name or None, + "project_bundle_name": project_bundle_name or None, "variant": variant or self.get_default_settings_variant() or None, "site_id": site_id, }) @@ -724,6 +726,7 @@ def get_bundle_settings( def get_addons_studio_settings( self, bundle_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, @@ -739,6 +742,8 @@ def get_addons_studio_settings( Args: bundle_name (Optional[str]): Name of bundle for which should be settings received. + project_bundle_name (Optional[str]): Project bundle name for + which should be settings received. variant (Optional[Literal['production', 'staging']]): Name of settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Site id for which want to receive @@ -755,6 +760,7 @@ def get_addons_studio_settings( """ output = self.get_bundle_settings( bundle_name=bundle_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site @@ -770,6 +776,7 @@ def get_addons_project_settings( self, project_name: str, bundle_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, use_site: bool = True, @@ -801,6 +808,8 @@ def get_addons_project_settings( received. bundle_name (Optional[str]): Name of bundle for which should be settings received. + project_bundle_name (Optional[str]): Project bundle name for which + should be settings received. variant (Optional[Literal['production', 'staging']]): Name of settings variant. Used 'default_settings_variant' by default. site_id (Optional[str]): Site id for which want to receive @@ -822,6 +831,7 @@ def get_addons_project_settings( output = self.get_bundle_settings( project_name=project_name, bundle_name=bundle_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site @@ -836,6 +846,7 @@ def get_addons_project_settings( def get_addons_settings( self, bundle_name: Optional[str] = None, + project_bundle_name: Optional[str] = None, project_name: Optional[str] = None, variant: Optional[str] = None, site_id: Optional[str] = None, @@ -855,6 +866,8 @@ def get_addons_settings( Args: bundle_name (Optional[str]): Name of bundle for which should be settings received. + project_bundle_name (Optional[str]): Name of project bundle + for which should be settings received. project_name (Optional[str]): Name of project for which should be settings received. variant (Optional[Literal['production', 'staging']]): Name of @@ -871,6 +884,7 @@ def get_addons_settings( if project_name is None: return self.get_addons_studio_settings( bundle_name=bundle_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site, @@ -880,6 +894,7 @@ def get_addons_settings( return self.get_addons_project_settings( project_name=project_name, bundle_name=bundle_name, + project_bundle_name=project_bundle_name, variant=variant, site_id=site_id, use_site=use_site, From 9d5ddd3cd40e1d4d582ff91f20e06dfe3000dc3f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:22:59 +0100 Subject: [PATCH 328/506] added support for pdf --- ayon_api/utils.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 7554c60fa..7fb636352 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1118,12 +1118,12 @@ def _get_media_mime_type_for_content_base(content: bytes) -> Optional[str]: content_len = len(content) # Pre-validation (largest definition check) # - hopefully there cannot be media defined in less than 12 bytes - if content_len < 12: + if content_len < 4: return None - # FTYP - if content[4:8] == b"ftyp": - return _get_media_mime_type_from_ftyp(content) + # PDF + if content[0:4] == b"%PDF": + return "application/pdf" # BMP if content[0:2] == b"BM": @@ -1162,6 +1162,14 @@ def _get_media_mime_type_for_content_base(content: bytes) -> Optional[str]: # with this header if content[0:4] == b"\x00\x00\x01\x00": return "image/x-icon" + + if content_len < 8: + return None + + # FTYP + if content[4:8] == b"ftyp": + return _get_media_mime_type_from_ftyp(content) + return None From 3e4ce9af3515c1f33c106ef770f3e509a6289eb0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:23:11 +0100 Subject: [PATCH 329/506] added support for json file --- ayon_api/utils.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 7fb636352..5f7bf07c4 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1180,23 +1180,32 @@ def _get_svg_mime_type(content: bytes) -> Optional[str]: return None +def _get_json_mime_type(content: bytes) -> Optional[str]: + # json + try: + json.loads(content.decode("utf-8")) + return "application/json" + except (UnicodeDecodeError, ValueError): + pass + return None + + def get_media_mime_type_for_content(content: bytes) -> Optional[str]: mime_type = _get_media_mime_type_for_content_base(content) if mime_type is not None: return mime_type - return _get_svg_mime_type(content) + return _get_svg_mime_type(content) or _get_json_mime_type(content) def get_media_mime_type_for_stream(stream: StreamType) -> Optional[str]: # Read only 12 bytes to determine mime type content = stream.read(12) - if len(content) < 12: - return None mime_type = _get_media_mime_type_for_content_base(content) - if mime_type is None: - content += stream.read() - mime_type = _get_svg_mime_type(content) - return mime_type + if mime_type is not None: + return mime_type + + content += stream.read() + return _get_svg_mime_type(content) or _get_json_mime_type(content) def get_media_mime_type(filepath: str) -> Optional[str]: From 9b14d6c2f87d325d0f75057fd16e83098efb08fb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:30:32 +0100 Subject: [PATCH 330/506] move handling of content type and xfilename to _upload_file --- ayon_api/server_api.py | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 03086e050..4ce59687b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1687,6 +1687,9 @@ def _upload_file( progress: TransferProgress, request_type: Optional[RequestType] = None, chunk_size: Optional[int] = None, + *, + content_type: Optional[str] = None, + filename: Optional[str] = None, **kwargs ) -> requests.Response: """Upload file to server. @@ -1714,11 +1717,14 @@ def _upload_file( url = self._endpoint_to_url(endpoint, use_rest=False) progress.set_destination_url(url) + headers = kwargs.setdefault("headers", {}) + headers_keys_by_low_key = {key.lower(): key for key in headers} if self._session is None: - headers = kwargs.setdefault("headers", {}) for key, value in self.get_headers().items(): - if key not in headers: + orig_key = headers_keys_by_low_key.get(key) + if not orig_key: headers[key] = value + post_func = self._base_functions_mapping[request_type] else: post_func = self._session_functions_mapping[request_type] @@ -1726,6 +1732,18 @@ def _upload_file( if not chunk_size: chunk_size = self.default_upload_chunk_size + for key, value in ( + ("x-file-name", filename), + ("Content-Type", content_type), + ): + if not value: + continue + + orig_key = headers_keys_by_low_key.get(key) + if orig_key: + headers.pop(orig_key) + headers[key] = filename + retries = self.get_default_max_retries() response = None @@ -1905,24 +1923,9 @@ def upload_reviewable( f"Could not determine MIME type of file '{filepath}'" ) - if headers is None: - headers = self.get_headers(content_type) - else: - # Make sure content-type is filled with file content type - content_type_key = next( - ( - key - for key in headers - if key.lower() == "content-type" - ), - "Content-Type" - ) - headers[content_type_key] = content_type - # Fill original filename if not explicitly defined if not filename: filename = os.path.basename(filepath) - headers["x-file-name"] = filename query = prepare_query_string({"label": label or None}) endpoint = ( From a1f6ed975766f701a35e4a60c11120350117ddc2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:31:12 +0100 Subject: [PATCH 331/506] handle mime type and filename --- ayon_api/server_api.py | 44 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 4ce59687b..80fa5fbdc 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -62,6 +62,7 @@ get_default_site_id, NOT_SET, get_media_mime_type, + get_media_mime_type_for_stream, get_machine_name, fill_own_attribs, ) @@ -1522,6 +1523,8 @@ def upload_project_file( project_name: str, filepath: str, *, + content_type: Optional[str] = None, + filename: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1534,6 +1537,9 @@ def upload_project_file( Args: project_name (str): Project name. filepath (str): Path where file will be downloaded. + content_type (Optional[str]): MIME type of file. + filename (Optional[str]): Server filename, filename from filepath + is used if not passed. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1543,9 +1549,18 @@ def upload_project_file( requests.Response: Requests response. """ + if not filename: + filename = os.path.basename(filepath) + + if not content_type: + content_type = get_media_mime_type(filepath) + if not content_type: + content_type = "application/octet-stream" + return self.upload_file( f"api/projects/{project_name}/files", filepath, + filename=filename, chunk_size=chunk_size, progress=progress, request_type=RequestTypes.post, @@ -1555,7 +1570,9 @@ def upload_project_file_from_stream( self, project_name: str, stream: StreamType, + filename: str, *, + content_type: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1568,6 +1585,8 @@ def upload_project_file_from_stream( Args: project_name (str): Project name. stream (StreamType): Stream used as source for upload. + filename (str): Name of file on server. + content_type (Optional[str]): MIME type of file. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1577,11 +1596,19 @@ def upload_project_file_from_stream( requests.Response: Requests response. """ + if not content_type: + stream.seek(0) + content_type = get_media_mime_type_for_stream(stream) + if not content_type: + content_type = "application/octet-stream" + return self.upload_file_from_stream( f"api/projects/{project_name}/files", stream, + filename=filename, chunk_size=chunk_size, progress=progress, + content_type=content_type, request_type=RequestTypes.post, ) @@ -1761,6 +1788,7 @@ def _upload_file( data=self._upload_chunks_iter( stream, progress, chunk_size ), + headers=headers, **kwargs ) # Auto-fix missing 'api/' @@ -1798,6 +1826,9 @@ def upload_file_from_stream( stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, + *, + filename: Optional[str] = None, + content_type: Optional[str] = None, **kwargs ) -> requests.Response: """Upload file to server from bytes. @@ -1813,6 +1844,8 @@ def upload_file_from_stream( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + filename (Optional[str]): Filename of file on server. + content_type (Optional[str]): MIME type of the file. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1833,6 +1866,8 @@ def upload_file_from_stream( stream, progress, request_type, + filename=filename, + content_type=content_type, **kwargs ) @@ -1849,6 +1884,9 @@ def upload_file( filepath: str, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, + *, + filename: Optional[str] = None, + content_type: Optional[str] = None, **kwargs ) -> requests.Response: """Upload file to server. @@ -1864,6 +1902,8 @@ def upload_file( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + content_type (Optional[str]): MIME type of the file. + filename (Optional[str]): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1882,6 +1922,8 @@ def upload_file( stream, progress, request_type, + filename=filename, + content_type=content_type, **kwargs ) @@ -1936,6 +1978,8 @@ def upload_reviewable( endpoint, filepath, progress=progress, + content_type=content_type, + filename=filename, headers=headers, request_type=RequestTypes.post, **kwargs From 304b7fbef2028e029b384272edcf2998c6073d5f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 10:31:47 +0100 Subject: [PATCH 332/506] update public api --- ayon_api/_api.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 6c1ba45bc..92946f43c 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -997,6 +997,8 @@ def upload_project_file( project_name: str, filepath: str, *, + content_type: Optional[str] = None, + filename: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1009,6 +1011,9 @@ def upload_project_file( Args: project_name (str): Project name. filepath (str): Path where file will be downloaded. + content_type (Optional[str]): MIME type of file. + filename (Optional[str]): Server filename, filename from filepath + is used if not passed. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1022,6 +1027,8 @@ def upload_project_file( return con.upload_project_file( project_name=project_name, filepath=filepath, + content_type=content_type, + filename=filename, chunk_size=chunk_size, progress=progress, ) @@ -1030,7 +1037,9 @@ def upload_project_file( def upload_project_file_from_stream( project_name: str, stream: StreamType, + filename: str, *, + content_type: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1043,6 +1052,8 @@ def upload_project_file_from_stream( Args: project_name (str): Project name. stream (StreamType): Stream used as source for upload. + filename (str): Name of file on server. + content_type (Optional[str]): MIME type of file. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1056,6 +1067,8 @@ def upload_project_file_from_stream( return con.upload_project_file_from_stream( project_name=project_name, stream=stream, + filename=filename, + content_type=content_type, chunk_size=chunk_size, progress=progress, ) @@ -1140,6 +1153,9 @@ def upload_file_from_stream( stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, + *, + filename: Optional[str] = None, + content_type: Optional[str] = None, **kwargs, ) -> requests.Response: """Upload file to server from bytes. @@ -1155,6 +1171,8 @@ def upload_file_from_stream( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + filename (Optional[str]): Filename of file on server. + content_type (Optional[str]): MIME type of the file. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1168,6 +1186,8 @@ def upload_file_from_stream( stream=stream, progress=progress, request_type=request_type, + filename=filename, + content_type=content_type, **kwargs, ) @@ -1177,6 +1197,9 @@ def upload_file( filepath: str, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, + *, + filename: Optional[str] = None, + content_type: Optional[str] = None, **kwargs, ) -> requests.Response: """Upload file to server. @@ -1192,6 +1215,8 @@ def upload_file( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. + content_type (Optional[str]): MIME type of the file. + filename (Optional[str]): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1205,6 +1230,8 @@ def upload_file( filepath=filepath, progress=progress, request_type=request_type, + filename=filename, + content_type=content_type, **kwargs, ) @@ -6955,6 +6982,7 @@ def create_link( output_id: str, output_type: str, link_name: Optional[str] = None, + data: Optional[dict[str, Any]] = None, ) -> CreateLinkData: """Create link between 2 entities. @@ -6974,7 +7002,8 @@ def create_link( output_id (str): Output entity id. output_type (str): Entity type of output entity. link_name (Optional[str]): Name of link. - Available from server version '1.0.0-rc.6'. + data (Optional[dict[str, Any]]): Additional data to be stored + with the link. Returns: CreateLinkData: Information about link. @@ -6992,6 +7021,7 @@ def create_link( output_id=output_id, output_type=output_type, link_name=link_name, + data=data, ) From d18b03951cf9a6de34b4c367dae35c0b18db110e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:45:56 +0100 Subject: [PATCH 333/506] unify arguments order --- ayon_api/server_api.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 80fa5fbdc..f2c6de0ce 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1827,8 +1827,8 @@ def upload_file_from_stream( progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, *, - filename: Optional[str] = None, content_type: Optional[str] = None, + filename: Optional[str] = None, **kwargs ) -> requests.Response: """Upload file to server from bytes. @@ -1844,8 +1844,8 @@ def upload_file_from_stream( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. - filename (Optional[str]): Filename of file on server. content_type (Optional[str]): MIME type of the file. + filename (Optional[str]): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1866,8 +1866,8 @@ def upload_file_from_stream( stream, progress, request_type, - filename=filename, content_type=content_type, + filename=filename, **kwargs ) @@ -1885,8 +1885,8 @@ def upload_file( progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, *, - filename: Optional[str] = None, content_type: Optional[str] = None, + filename: Optional[str] = None, **kwargs ) -> requests.Response: """Upload file to server. @@ -1922,8 +1922,8 @@ def upload_file( stream, progress, request_type, - filename=filename, content_type=content_type, + filename=filename, **kwargs ) From c6673cd3b6c8f533bbb104ede885c590e21448c9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:46:14 +0100 Subject: [PATCH 334/506] added delete project file helper --- ayon_api/server_api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f2c6de0ce..de7ec832d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1682,6 +1682,11 @@ def download_project_file_to_stream( progress=progress, ) + def delete_project_file(self, project_name: str, file_id: str) -> None: + """Delete project file.""" + response = self.delete(f"projects/{project_name}/files/{file_id}") + response.raise_for_status() + @staticmethod def _upload_chunks_iter( file_stream: StreamType, From 8b96a92d9ba0f29166d1621d0479c1ef8e9e63a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:46:57 +0100 Subject: [PATCH 335/506] fix arguments order passed to upload --- ayon_api/server_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index de7ec832d..6b90435be 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1560,6 +1560,7 @@ def upload_project_file( return self.upload_file( f"api/projects/{project_name}/files", filepath, + content_type=content_type, filename=filename, chunk_size=chunk_size, progress=progress, @@ -1605,10 +1606,10 @@ def upload_project_file_from_stream( return self.upload_file_from_stream( f"api/projects/{project_name}/files", stream, + content_type=content_type, filename=filename, chunk_size=chunk_size, progress=progress, - content_type=content_type, request_type=RequestTypes.post, ) From 44aa7ee0bc32764127716168c0ddd9285d322719 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:47:09 +0100 Subject: [PATCH 336/506] fix headers preparation --- ayon_api/server_api.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6b90435be..6c13983ec 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1771,11 +1771,10 @@ def _upload_file( ): if not value: continue - - orig_key = headers_keys_by_low_key.get(key) + orig_key = headers_keys_by_low_key.get(key.lower()) if orig_key: headers.pop(orig_key) - headers[key] = filename + headers[key] = value retries = self.get_default_max_retries() response = None From 5ca0a669402d5345d65ee6fe7a8a5875608bde6d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:47:16 +0100 Subject: [PATCH 337/506] pass headers only once --- ayon_api/server_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6c13983ec..a318738b6 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1793,7 +1793,6 @@ def _upload_file( data=self._upload_chunks_iter( stream, progress, chunk_size ), - headers=headers, **kwargs ) # Auto-fix missing 'api/' From 67c41d2f28bdfb5390476fbab1d05ec9c8f1e6c1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:47:29 +0100 Subject: [PATCH 338/506] support query parameters --- ayon_api/server_api.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a318738b6..b97e39f18 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1525,6 +1525,8 @@ def upload_project_file( *, content_type: Optional[str] = None, filename: Optional[str] = None, + file_id: Optional[str] = None, + activity_id: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1540,6 +1542,8 @@ def upload_project_file( content_type (Optional[str]): MIME type of file. filename (Optional[str]): Server filename, filename from filepath is used if not passed. + file_id (Optional[str]): File id. + activity_id (Optional[str]): To which activity is file related. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1557,8 +1561,12 @@ def upload_project_file( if not content_type: content_type = "application/octet-stream" + query = prepare_query_string({ + "x_file_id": file_id, + "x_activity_id": activity_id, + }) return self.upload_file( - f"api/projects/{project_name}/files", + f"api/projects/{project_name}/files{query}", filepath, content_type=content_type, filename=filename, @@ -1574,6 +1582,8 @@ def upload_project_file_from_stream( filename: str, *, content_type: Optional[str] = None, + file_id: Optional[str] = None, + activity_id: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1588,6 +1598,8 @@ def upload_project_file_from_stream( stream (StreamType): Stream used as source for upload. filename (str): Name of file on server. content_type (Optional[str]): MIME type of file. + file_id (Optional[str]): File id. + activity_id (Optional[str]): To which activity is file related. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1603,8 +1615,12 @@ def upload_project_file_from_stream( if not content_type: content_type = "application/octet-stream" + query = prepare_query_string({ + "x_file_id": file_id, + "x_activity_id": activity_id, + }) return self.upload_file_from_stream( - f"api/projects/{project_name}/files", + f"api/projects/{project_name}/files{query}", stream, content_type=content_type, filename=filename, From 842b620a82f4172320ebc8f51707f08021c4769c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 27 Feb 2026 11:48:31 +0100 Subject: [PATCH 339/506] update public api --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 35 ++++++++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index ae73d2090..0c52eb8ba 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -74,6 +74,7 @@ upload_project_file_from_stream, download_project_file, download_project_file_to_stream, + delete_project_file, upload_file_from_stream, upload_file, upload_reviewable, @@ -357,6 +358,7 @@ "upload_project_file_from_stream", "download_project_file", "download_project_file_to_stream", + "delete_project_file", "upload_file_from_stream", "upload_file", "upload_reviewable", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index cb6becb4c..97833c8c8 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -999,6 +999,8 @@ def upload_project_file( *, content_type: Optional[str] = None, filename: Optional[str] = None, + file_id: Optional[str] = None, + activity_id: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1014,6 +1016,8 @@ def upload_project_file( content_type (Optional[str]): MIME type of file. filename (Optional[str]): Server filename, filename from filepath is used if not passed. + file_id (Optional[str]): File id. + activity_id (Optional[str]): To which activity is file related. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1029,6 +1033,8 @@ def upload_project_file( filepath=filepath, content_type=content_type, filename=filename, + file_id=file_id, + activity_id=activity_id, chunk_size=chunk_size, progress=progress, ) @@ -1040,6 +1046,8 @@ def upload_project_file_from_stream( filename: str, *, content_type: Optional[str] = None, + file_id: Optional[str] = None, + activity_id: Optional[str] = None, chunk_size: Optional[int] = None, progress: Optional[TransferProgress] = None, ) -> requests.Response: @@ -1054,6 +1062,8 @@ def upload_project_file_from_stream( stream (StreamType): Stream used as source for upload. filename (str): Name of file on server. content_type (Optional[str]): MIME type of file. + file_id (Optional[str]): File id. + activity_id (Optional[str]): To which activity is file related. chunk_size (Optional[int]): Size of chunks that are received in single loop. progress (Optional[TransferProgress]): Object that gives ability @@ -1069,6 +1079,8 @@ def upload_project_file_from_stream( stream=stream, filename=filename, content_type=content_type, + file_id=file_id, + activity_id=activity_id, chunk_size=chunk_size, progress=progress, ) @@ -1148,14 +1160,27 @@ def download_project_file_to_stream( ) +def delete_project_file( + project_name: str, + file_id: str, +) -> None: + """Delete project file. + """ + con = get_server_api_connection() + return con.delete_project_file( + project_name=project_name, + file_id=file_id, + ) + + def upload_file_from_stream( endpoint: str, stream: StreamType, progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, *, - filename: Optional[str] = None, content_type: Optional[str] = None, + filename: Optional[str] = None, **kwargs, ) -> requests.Response: """Upload file to server from bytes. @@ -1171,8 +1196,8 @@ def upload_file_from_stream( to track upload progress. request_type (Optional[RequestType]): Type of request that will be used to upload file. - filename (Optional[str]): Filename of file on server. content_type (Optional[str]): MIME type of the file. + filename (Optional[str]): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1186,8 +1211,8 @@ def upload_file_from_stream( stream=stream, progress=progress, request_type=request_type, - filename=filename, content_type=content_type, + filename=filename, **kwargs, ) @@ -1198,8 +1223,8 @@ def upload_file( progress: Optional[TransferProgress] = None, request_type: Optional[RequestType] = None, *, - filename: Optional[str] = None, content_type: Optional[str] = None, + filename: Optional[str] = None, **kwargs, ) -> requests.Response: """Upload file to server. @@ -1230,8 +1255,8 @@ def upload_file( filepath=filepath, progress=progress, request_type=request_type, - filename=filename, content_type=content_type, + filename=filename, **kwargs, ) From d84fd6f140f8f65347dba14b282be2978f43101a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 11:57:42 +0100 Subject: [PATCH 340/506] added CI action to validate global api changes --- .github/workflows/check_global_api.yml | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/check_global_api.yml diff --git a/.github/workflows/check_global_api.yml b/.github/workflows/check_global_api.yml new file mode 100644 index 000000000..cdbb600cf --- /dev/null +++ b/.github/workflows/check_global_api.yml @@ -0,0 +1,34 @@ +name: Check Global API + +on: + pull_request: + branches: [ develop ] + +jobs: + check-api: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests Unidecode + + - name: Run automated_api.py + run: python automated_api.py + + - name: Check for changes + run: | + if [ -n "$(git status --porcelain)" ]; then + echo "Error: Running automated_api.py resulted in code changes." + echo "Please run 'python automated_api.py' locally and commit the changes." + git status + git diff + exit 1 + fi From 98a262603ae7d3b585dc80fc64d053a0e41bb7e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:56:02 +0100 Subject: [PATCH 341/506] added CI action to release new version --- .github/workflows/create_release.yml | 78 ++++++++++++++++++++ manage_version.py | 103 +++++++++++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 .github/workflows/create_release.yml create mode 100644 manage_version.py diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml new file mode 100644 index 000000000..82c2b02da --- /dev/null +++ b/.github/workflows/create_release.yml @@ -0,0 +1,78 @@ +name: 🚀 Create Release + +on: + workflow_dispatch: + inputs: + bump_minor: + description: 'Bump minor version' + required: false + type: boolean + default: false + version: + description: 'Release version (optional, if not provided will use version from develop without -dev)' + required: false + type: string + +jobs: + release: + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.9' + + - name: Configure Git + run: | + git config --global user.name "${{ secrets.CI_USER }}" + git config --global user.email "${{ secrets.CI_EMAIL }}" + + - name: Get release version + id: get_version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT + else + ARGS="" + if [ "${{ github.event.inputs.bump_minor }}" = "true" ]; then + ARGS="--bump-minor" + fi + VERSION=$(python manage_version.py get-release-version $ARGS) + echo "version=$VERSION" >> $GITHUB_OUTPUT + fi + + - name: Update version for release + run: | + python manage_version.py update --version ${{ steps.get_version.outputs.version }} + + - name: Commit release version + run: | + git add ayon_api/version.py pyproject.toml + git commit -m "Release version ${{ steps.get_version.outputs.version }}" + git push origin develop + + - name: Rebase main on develop + run: | + git checkout main + git rebase develop + git push origin main + + - name: Create and push tag + run: | + git tag ${{ steps.get_version.outputs.version }} + git push origin ${{ steps.get_version.outputs.version }} + + - name: Bump version on develop + run: | + git checkout develop + NEW_VERSION=$(python manage_version.py get-dev-version) + python manage_version.py update --version $NEW_VERSION + git add ayon_api/version.py pyproject.toml + git commit -m "Bump version to $NEW_VERSION" + git push origin develop diff --git a/manage_version.py b/manage_version.py new file mode 100644 index 000000000..1ccee79fa --- /dev/null +++ b/manage_version.py @@ -0,0 +1,103 @@ +import re +import sys +import argparse +from pathlib import Path + +SEMVER_REGEX = re.compile( + "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" + "(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + "(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" +) + + +def get_current_version(version_file: Path) -> str: + content = version_file.read_text(encoding="utf-8") + match = re.search(r'__version__\s*=\s*"(.*)"', content) + if match: + return match.group(1) + raise ValueError(f"Version not found in {version_file}") + + +def update_version_in_src( + version_file: Path, + pyproject_file: Path, + new_version: str, +): + content = version_file.read_text(encoding="utf-8") + new_content = re.sub( + r'(__version__\s*=\s*").*(")', + rf'\g<1>{new_version}\g<2>', + content + ) + version_file.write_text(new_content, encoding="utf-8") + + # Update pyproject.toml + content = pyproject_file.read_text(encoding="utf-8") + new_lines = [] + for line in content.splitlines(): + if line.startswith("version"): + line = f"version = \"{new_version}\"" + new_lines.append(line) + + pyproject_file.write_text("\n".join(new_lines), encoding="utf-8") + + +def bump_to_dev_version(version: str) -> str: + version_parts = SEMVER_REGEX.match(version).groups() + major, minor, patch = version_parts[0:3] + patch = str(int(patch) + 1) + return f"{major}.{minor}.{patch}-dev" + + +def bump_to_release_version(version: str, bump_minor: bool = False) -> str: + version_parts = SEMVER_REGEX.match(version).groups() + major, minor, patch = version_parts[0:3] + if bump_minor: + minor = str(int(minor) + 1) + patch = "0" + return f"{major}.{minor}.{patch}" + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "command", + choices=["get-release-version", "update", "get-dev-version"] + ) + parser.add_argument( + "--version", + help="Version to set for update command", + ) + parser.add_argument( + "--bump-minor", + action="store_true", + help="Bump minor version for get-release-version command", + ) + args = parser.parse_args() + + repo_root = Path(__file__).parent + version_file = repo_root / "ayon_api" / "version.py" + pyproject_file = repo_root / "pyproject.toml" + + current_version = get_current_version(version_file) + + if args.command == "get-release-version": + if current_version: + print(bump_to_release_version(current_version, args.bump_minor)) + else: + sys.exit(1) + elif args.command == "get-dev-version": + if current_version: + print(bump_to_dev_version(current_version)) + else: + sys.exit(1) + elif args.command == "update": + if not args.version: + print("Error: --version is required for update command") + sys.exit(1) + update_version_in_src(version_file, pyproject_file, args.version) + print(f"Updated version to {args.version}") + + +if __name__ == "__main__": + main() From 8ab1bd98fdf0fdcc99367a558f70cb10575332c4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:02:21 +0100 Subject: [PATCH 342/506] make release --- .github/workflows/create_release.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 82c2b02da..67688cbc4 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -17,6 +17,8 @@ jobs: release: runs-on: ubuntu-latest if: github.ref == 'refs/heads/develop' + permissions: + contents: write steps: - uses: actions/checkout@v4 with: @@ -68,6 +70,13 @@ jobs: git tag ${{ steps.get_version.outputs.version }} git push origin ${{ steps.get_version.outputs.version }} + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.version }} + generate_release_notes: true + token: ${{ secrets.GITHUB_TOKEN }} + - name: Bump version on develop run: | git checkout develop From 74925c7438b93bbdc00168220db727a0cc1d42ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:03:11 +0100 Subject: [PATCH 343/506] move release creation as last step --- .github/workflows/create_release.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 67688cbc4..934f23792 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -70,13 +70,6 @@ jobs: git tag ${{ steps.get_version.outputs.version }} git push origin ${{ steps.get_version.outputs.version }} - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ steps.get_version.outputs.version }} - generate_release_notes: true - token: ${{ secrets.GITHUB_TOKEN }} - - name: Bump version on develop run: | git checkout develop @@ -85,3 +78,10 @@ jobs: git add ayon_api/version.py pyproject.toml git commit -m "Bump version to $NEW_VERSION" git push origin develop + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.version }} + generate_release_notes: true + token: ${{ secrets.GITHUB_TOKEN }} From cdbe566b65612d2680a5e12110691a7613f837ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:05:13 +0100 Subject: [PATCH 344/506] added regex marking --- manage_version.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manage_version.py b/manage_version.py index 1ccee79fa..2f780d176 100644 --- a/manage_version.py +++ b/manage_version.py @@ -4,9 +4,9 @@ from pathlib import Path SEMVER_REGEX = re.compile( - "^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" - "(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" - "(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" + r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)" + r"(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?" + r"(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$" ) From 200edc4d7ae479052e342b71fc89453321aeb812 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:21:57 +0100 Subject: [PATCH 345/506] Define github token with 'YNPUT_BOT_TOKEN' --- .github/workflows/create_release.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 934f23792..a95f235fa 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -19,11 +19,12 @@ jobs: if: github.ref == 'refs/heads/develop' permissions: contents: write + env: + GITHUB_TOKEN: ${{ secrets.YNPUT_BOT_TOKEN }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - token: ${{ secrets.GITHUB_TOKEN }} - name: Set up Python uses: actions/setup-python@v5 @@ -84,4 +85,3 @@ jobs: with: tag_name: ${{ steps.get_version.outputs.version }} generate_release_notes: true - token: ${{ secrets.GITHUB_TOKEN }} From 645650f9fba905009bf76bf68c870f63930c3aa0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:23:11 +0100 Subject: [PATCH 346/506] shorter description --- .github/workflows/create_release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index a95f235fa..e7e9415b0 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -9,7 +9,7 @@ on: type: boolean default: false version: - description: 'Release version (optional, if not provided will use version from develop without -dev)' + description: 'Release version (optional)' required: false type: string From 70e92871ce09b32906c212d5470912ce4c6e50ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:30:43 +0100 Subject: [PATCH 347/506] try to use push protected --- .github/workflows/create_release.yml | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index e7e9415b0..5aac8a0b9 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -58,13 +58,25 @@ jobs: run: | git add ayon_api/version.py pyproject.toml git commit -m "Release version ${{ steps.get_version.outputs.version }}" - git push origin develop + + - name: Push to protected develop branch + uses: CasperWA/push-protected@v2.10.0 + with: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + branch: develop + unprotect_reviews: true - name: Rebase main on develop run: | git checkout main git rebase develop - git push origin main + + - name: Push to protected main branch + uses: CasperWA/push-protected@v2.10.0 + with: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + branch: main + unprotect_reviews: true - name: Create and push tag run: | @@ -78,7 +90,13 @@ jobs: python manage_version.py update --version $NEW_VERSION git add ayon_api/version.py pyproject.toml git commit -m "Bump version to $NEW_VERSION" - git push origin develop + + - name: Push to protected develop branch + uses: CasperWA/push-protected@v2.10.0 + with: + token: ${{ secrets.YNPUT_BOT_TOKEN }} + branch: develop + unprotect_reviews: true - name: Create GitHub Release uses: softprops/action-gh-release@v2 From 78f619fef67825430697b1441695a7eace51e589 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 10 Mar 2026 14:31:54 +0000 Subject: [PATCH 348/506] Release version 1.2.12 --- ayon_api/version.py | 2 +- pyproject.toml | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index bd13d6f49..52bd4b934 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.12-dev" +__version__ = "1.2.12" diff --git a/pyproject.toml b/pyproject.toml index 92183a85d..d7229f95c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.12-dev" +version = "1.2.12" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.2" +version = "1.2.12" description = "AYON Python API" authors = [ "ynput.io " @@ -49,4 +49,4 @@ mock = "*" sphinx-autoapi = "*" revitron-sphinx-theme = { git = "https://github.com/revitron/revitron-sphinx-theme.git", branch = "master" } pytest = "^6.2.5" -pydocstyle = "^6.3.0" +pydocstyle = "^6.3.0" \ No newline at end of file From 20f72154b24ffc83af51d0668c7c1fd4443f889d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 10 Mar 2026 14:32:13 +0000 Subject: [PATCH 349/506] Bump version to 1.2.13-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 52bd4b934..dfab06ada 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.12" +__version__ = "1.2.13-dev" diff --git a/pyproject.toml b/pyproject.toml index d7229f95c..56a1c567d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.12" +version = "1.2.13-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.12" +version = "1.2.13-dev" description = "AYON Python API" authors = [ "ynput.io " From 3d7d2b1ba9246cbf735d3d2dd3a0cfb7103cfca7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:17:02 +0100 Subject: [PATCH 350/506] implemente helper to find out if links data can be fetched --- ayon_api/_api_helpers/base.py | 3 +++ ayon_api/server_api.py | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index 0a3d19550..d2441bb14 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -28,6 +28,9 @@ def log(self) -> logging.Logger: def is_product_base_type_supported(self) -> bool: raise NotImplementedError() + def links_graphql_support_data(self) -> bool: + raise NotImplementedError() + def get_server_version(self) -> str: raise NotImplementedError() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index b97e39f18..867a09efe 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -329,6 +329,7 @@ def __init__( self._graphql_allows_traits_in_representations: Optional[bool] = None self._product_base_type_supported = None + self._links_graphql_support_data = None self._session = None @@ -922,6 +923,15 @@ def is_product_base_type_supported(self) -> bool: ) return self._product_base_type_supported + def links_graphql_support_data(self) -> bool: + """Links data can be received by GraphQl.""" + if self._links_graphql_support_data is None: + major, minor, patch, _, _ = self.server_version_tuple + self._links_graphql_support_data = ( + (major, minor, patch) >= (1, 14, 2) + ) + return self._links_graphql_support_data + def _get_user_info(self) -> Optional[dict[str, Any]]: if self._access_token is None: return None From 26c9121d3e253c473c688bde26d4294d882df088 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:23:36 +0100 Subject: [PATCH 351/506] graphql queries can pass in if supports links data --- ayon_api/graphql_queries.py | 101 ++++++++++++++++++++++++++++++------ 1 file changed, 86 insertions(+), 15 deletions(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index df0a1e69e..9d5ceb0c4 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -1,15 +1,30 @@ +from __future__ import annotations + import collections +import typing from .constants import DEFAULT_LINK_FIELDS from .graphql import FIELD_VALUE, GraphQlQuery, fields_to_dict +if typing.TYPE_CHECKING: + from .graphql import ( + GraphQlQueryEdgeField, + ) + -def add_links_fields(entity_field, nested_fields): +def add_links_fields( + entity_field: GraphQlQueryEdgeField, + nested_fields: dict | None, + supports_data: bool = False, +) -> None: if "links" not in nested_fields: return links_fields = nested_fields.pop("links") link_edge_fields = set(DEFAULT_LINK_FIELDS) + if supports_data: + link_edge_fields.add("data") + if isinstance(links_fields, dict): simple_fields = set(links_fields) simple_variant = len(simple_fields - link_edge_fields) == 0 @@ -121,7 +136,11 @@ def product_types_query(fields): return query -def folders_graphql_query(fields): +def folders_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("FoldersQuery") project_name_var = query.add_variable("projectName", "String!") folder_ids_var = query.add_variable("folderIds", "[String!]") @@ -161,7 +180,11 @@ def folders_graphql_query(fields): folders_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) - add_links_fields(folders_field, nested_fields) + add_links_fields( + folders_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -179,7 +202,11 @@ def folders_graphql_query(fields): return query -def tasks_graphql_query(fields): +def tasks_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("TasksQuery") project_name_var = query.add_variable("projectName", "String!") task_ids_var = query.add_variable("taskIds", "[String!]") @@ -209,7 +236,11 @@ def tasks_graphql_query(fields): tasks_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) - add_links_fields(tasks_field, nested_fields) + add_links_fields( + tasks_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -227,7 +258,11 @@ def tasks_graphql_query(fields): return query -def tasks_by_folder_paths_graphql_query(fields): +def tasks_by_folder_paths_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("TasksByFolderPathQuery") project_name_var = query.add_variable("projectName", "String!") task_names_var = query.add_variable("taskNames", "[String!]") @@ -258,7 +293,11 @@ def tasks_by_folder_paths_graphql_query(fields): tasks_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) - add_links_fields(tasks_field, nested_fields) + add_links_fields( + tasks_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -276,7 +315,11 @@ def tasks_by_folder_paths_graphql_query(fields): return query -def products_graphql_query(fields): +def products_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("ProductsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -308,7 +351,11 @@ def products_graphql_query(fields): products_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields(products_field, nested_fields) + add_links_fields( + products_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -326,7 +373,11 @@ def products_graphql_query(fields): return query -def versions_graphql_query(fields): +def versions_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("VersionsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -359,7 +410,11 @@ def versions_graphql_query(fields): versions_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields(versions_field, nested_fields) + add_links_fields( + versions_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -377,7 +432,11 @@ def versions_graphql_query(fields): return query -def representations_graphql_query(fields): +def representations_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("RepresentationsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -408,7 +467,11 @@ def representations_graphql_query(fields): repres_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields(repres_field, nested_fields) + add_links_fields( + repres_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -525,7 +588,11 @@ def representations_hierarchy_qraphql_query( return query -def workfiles_info_graphql_query(fields): +def workfiles_info_graphql_query( + fields: set[str], + *, + links_support_data: bool = False, +) -> GraphQlQuery: query = GraphQlQuery("WorkfilesInfo") project_name_var = query.add_variable("projectName", "String!") workfiles_info_ids = query.add_variable("workfileIds", "[String!]") @@ -549,7 +616,11 @@ def workfiles_info_graphql_query(fields): workfiles_field.set_filter("tags", tags_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields(workfiles_field, nested_fields) + add_links_fields( + workfiles_field, + nested_fields, + supports_data=links_support_data, + ) query_queue = collections.deque() for key, value in nested_fields.items(): From 528c125bd7cbd59592e03bdc4cbf76fb9c169872 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:25:45 +0100 Subject: [PATCH 352/506] pass information if server supports links data --- ayon_api/_api_helpers/folders.py | 10 ++++++++-- ayon_api/_api_helpers/links.py | 5 ++++- ayon_api/_api_helpers/products.py | 5 ++++- ayon_api/_api_helpers/representations.py | 5 ++++- ayon_api/_api_helpers/tasks.py | 10 ++++++++-- ayon_api/_api_helpers/versions.py | 15 ++++++++++++--- ayon_api/_api_helpers/workfiles.py | 5 ++++- 7 files changed, 44 insertions(+), 11 deletions(-) diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index 71535bdd5..8d27ee957 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -331,7 +331,10 @@ def get_folders( if own_attributes: fields.add("ownAttrib") - query = folders_graphql_query(fields) + query = folders_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) @@ -478,7 +481,10 @@ def get_folder_ids_with_products( if not folder_ids: return set() - query = folders_graphql_query({"id"}) + query = folders_graphql_query( + {"id"}, + links_support_data=self.links_graphql_support_data(), + ) query.set_variable_value("projectName", project_name) query.set_variable_value("folderHasProducts", True) if folder_ids: diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index dc73f7890..de47107b5 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -362,7 +362,10 @@ def get_entities_links( return output link_fields = {"id", "links"} - query = query_func(link_fields) + query = query_func( + link_fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index db1e29a0c..f3c116be2 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -179,7 +179,10 @@ def get_products( if filter_value: graphql_filters[filter_key] = filter_value - query = products_graphql_query(fields) + query = products_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/representations.py b/ayon_api/_api_helpers/representations.py index 56ac8e372..c98070bb7 100644 --- a/ayon_api/_api_helpers/representations.py +++ b/ayon_api/_api_helpers/representations.py @@ -168,7 +168,10 @@ def get_representations( if filters: graphql_filters["filter"] = filters - query = representations_graphql_query(fields) + query = representations_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index 4109d72ed..b90224d6b 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -106,7 +106,10 @@ def get_tasks( if active is not None: fields.add("active") - query = tasks_graphql_query(fields) + query = tasks_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) @@ -267,7 +270,10 @@ def get_tasks_by_folder_paths( if active is not None: fields.add("active") - query = tasks_by_folder_paths_graphql_query(fields) + query = tasks_by_folder_paths_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/versions.py b/ayon_api/_api_helpers/versions.py index 97800451c..fcef21ac7 100644 --- a/ayon_api/_api_helpers/versions.py +++ b/ayon_api/_api_helpers/versions.py @@ -130,14 +130,20 @@ def get_versions( if standard and not latest: # This query all versions standard + hero # - hero must be filtered out if is not enabled during loop - query = versions_graphql_query(fields) + query = versions_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) queries.append(query) else: if hero: # Add hero query if hero is enabled - hero_query = versions_graphql_query(fields) + hero_query = versions_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): hero_query.set_variable_value(attr, filter_value) @@ -145,7 +151,10 @@ def get_versions( queries.append(hero_query) if standard: - standard_query = versions_graphql_query(fields) + standard_query = versions_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in graphql_filters.items(): standard_query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index a0e35b9a8..d111f2906 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -93,7 +93,10 @@ def get_workfile_entities( fields = set(fields) self._prepare_fields("workfile", fields) - query = workfiles_info_graphql_query(fields) + query = workfiles_info_graphql_query( + fields, + links_support_data=self.links_graphql_support_data(), + ) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) From f7e1b6137a0d427a8c8f443ada29fa2dda21fc11 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:32:12 +0100 Subject: [PATCH 353/506] supports_data is kwarg --- ayon_api/graphql_queries.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 9d5ceb0c4..50bc127d6 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -15,6 +15,7 @@ def add_links_fields( entity_field: GraphQlQueryEdgeField, nested_fields: dict | None, + *, supports_data: bool = False, ) -> None: if "links" not in nested_fields: From 81fdc869893b4972df085a7575dc24ec71f5ab07 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 13 Mar 2026 15:35:03 +0100 Subject: [PATCH 354/506] update public api --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 0c52eb8ba..fc89a1dfb 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -55,6 +55,7 @@ get_server_version, get_server_version_tuple, is_product_base_type_supported, + links_graphql_support_data, get_users, get_user_by_name, get_user, @@ -339,6 +340,7 @@ "get_server_version", "get_server_version_tuple", "is_product_base_type_supported", + "links_graphql_support_data", "get_users", "get_user_by_name", "get_user", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 97833c8c8..a00bb04c0 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -729,6 +729,13 @@ def is_product_base_type_supported() -> bool: return con.is_product_base_type_supported() +def links_graphql_support_data() -> bool: + """Links data can be received by GraphQl. + """ + con = get_server_api_connection() + return con.links_graphql_support_data() + + def get_users( project_name: Optional[str] = None, usernames: Optional[Iterable[str]] = None, From d13784c21ff1491e3f897cd02b9f4b064f9cc272 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Mar 2026 14:06:19 +0100 Subject: [PATCH 355/506] use background operations for operations session --- ayon_api/operations.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/operations.py b/ayon_api/operations.py index 3cf8d1cff..e39167a80 100644 --- a/ayon_api/operations.py +++ b/ayon_api/operations.py @@ -834,8 +834,8 @@ def commit(self) -> None: if body is not None: operations_body.append(body) - self._con.send_batch_operations( - project_name, operations_body, can_fail=False + self._con.send_background_batch_operations( + project_name, operations_body, wait=True, can_fail=False ) def create_entity( From 3f694895ef63dd5f7febcb44fc77fea76419ea89 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:08:36 +0100 Subject: [PATCH 356/506] prepare links fields preparation --- ayon_api/_api_helpers/base.py | 3 +++ ayon_api/server_api.py | 12 ++++++++++++ 2 files changed, 15 insertions(+) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index d2441bb14..8ef198c68 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -145,6 +145,9 @@ def _prepare_fields( ): raise NotImplementedError() + def _prepare_link_fields(self, fields: set[str]) -> None: + raise NotImplementedError() + def _prepare_advanced_filters( self, filters: Union[str, dict[str, Any], None] ) -> Optional[str]: diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 867a09efe..db5f49b1f 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -38,6 +38,7 @@ DEFAULT_ACTIVITY_FIELDS, DEFAULT_USER_FIELDS, DEFAULT_ENTITY_LIST_FIELDS, + DEFAULT_LINK_FIELDS, ) from .graphql import INTROSPECTION_QUERY from .graphql_queries import users_graphql_query @@ -2455,6 +2456,17 @@ def _prepare_fields( ) } + def _prepare_link_fields(self, fields: set[str]) -> None: + if "links" not in fields: + return + + fields.discard("links") + for field in DEFAULT_LINK_FIELDS: + fields.add(f"links.{field}") + + if self.links_graphql_support_data(): + fields.add(f"links.data") + def _prepare_advanced_filters( self, filters: Union[str, dict[str, Any], None] ) -> Optional[str]: From 80420465b5a6855261caa18cd12058c19d93901a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:09:40 +0100 Subject: [PATCH 357/506] change how links fields are handled --- ayon_api/_api_helpers/folders.py | 12 +-- ayon_api/_api_helpers/products.py | 7 +- ayon_api/_api_helpers/representations.py | 7 +- ayon_api/_api_helpers/tasks.py | 14 ++-- ayon_api/_api_helpers/versions.py | 18 ++--- ayon_api/_api_helpers/workfiles.py | 7 +- ayon_api/graphql_queries.py | 95 +++++------------------- 7 files changed, 43 insertions(+), 117 deletions(-) diff --git a/ayon_api/_api_helpers/folders.py b/ayon_api/_api_helpers/folders.py index 8d27ee957..d10cfeb7a 100644 --- a/ayon_api/_api_helpers/folders.py +++ b/ayon_api/_api_helpers/folders.py @@ -331,10 +331,9 @@ def get_folders( if own_attributes: fields.add("ownAttrib") - query = folders_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + self._prepare_link_fields(fields) + + query = folders_graphql_query(fields) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) @@ -481,10 +480,7 @@ def get_folder_ids_with_products( if not folder_ids: return set() - query = folders_graphql_query( - {"id"}, - links_support_data=self.links_graphql_support_data(), - ) + query = folders_graphql_query({"id"}) query.set_variable_value("projectName", project_name) query.set_variable_value("folderHasProducts", True) if folder_ids: diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index f3c116be2..2eacb7cd7 100644 --- a/ayon_api/_api_helpers/products.py +++ b/ayon_api/_api_helpers/products.py @@ -179,10 +179,9 @@ def get_products( if filter_value: graphql_filters[filter_key] = filter_value - query = products_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + self._prepare_link_fields(fields) + + query = products_graphql_query(fields) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/representations.py b/ayon_api/_api_helpers/representations.py index c98070bb7..4535fa633 100644 --- a/ayon_api/_api_helpers/representations.py +++ b/ayon_api/_api_helpers/representations.py @@ -108,6 +108,8 @@ def get_representations( fields.discard("files") fields |= REPRESENTATION_FILES_FIELDS + self._prepare_link_fields(fields) + graphql_filters = { "projectName": project_name } @@ -168,10 +170,7 @@ def get_representations( if filters: graphql_filters["filter"] = filters - query = representations_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + query = representations_graphql_query(fields) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index b90224d6b..4ffa2dd5f 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -106,10 +106,9 @@ def get_tasks( if active is not None: fields.add("active") - query = tasks_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + self._prepare_link_fields(fields) + + query = tasks_graphql_query(fields) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) @@ -270,10 +269,9 @@ def get_tasks_by_folder_paths( if active is not None: fields.add("active") - query = tasks_by_folder_paths_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + self._prepare_link_fields(fields) + + query = tasks_by_folder_paths_graphql_query(fields) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/versions.py b/ayon_api/_api_helpers/versions.py index fcef21ac7..8a355fca1 100644 --- a/ayon_api/_api_helpers/versions.py +++ b/ayon_api/_api_helpers/versions.py @@ -87,6 +87,8 @@ def get_versions( if active is not None: fields.add("active") + self._prepare_link_fields(fields) + if own_attributes is not _PLACEHOLDER: warnings.warn( ( @@ -116,7 +118,6 @@ def get_versions( ): return - filters = self._prepare_advanced_filters(filters) if filters: graphql_filters["filter"] = filters @@ -130,20 +131,14 @@ def get_versions( if standard and not latest: # This query all versions standard + hero # - hero must be filtered out if is not enabled during loop - query = versions_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + query = versions_graphql_query(fields) for attr, filter_value in graphql_filters.items(): query.set_variable_value(attr, filter_value) queries.append(query) else: if hero: # Add hero query if hero is enabled - hero_query = versions_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + hero_query = versions_graphql_query(fields) for attr, filter_value in graphql_filters.items(): hero_query.set_variable_value(attr, filter_value) @@ -151,10 +146,7 @@ def get_versions( queries.append(hero_query) if standard: - standard_query = versions_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + standard_query = versions_graphql_query(fields) for attr, filter_value in graphql_filters.items(): standard_query.set_variable_value(attr, filter_value) diff --git a/ayon_api/_api_helpers/workfiles.py b/ayon_api/_api_helpers/workfiles.py index d111f2906..67991da54 100644 --- a/ayon_api/_api_helpers/workfiles.py +++ b/ayon_api/_api_helpers/workfiles.py @@ -93,10 +93,9 @@ def get_workfile_entities( fields = set(fields) self._prepare_fields("workfile", fields) - query = workfiles_info_graphql_query( - fields, - links_support_data=self.links_graphql_support_data(), - ) + self._prepare_link_fields(fields) + + query = workfiles_info_graphql_query(fields) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 50bc127d6..27719d389 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -15,20 +15,17 @@ def add_links_fields( entity_field: GraphQlQueryEdgeField, nested_fields: dict | None, - *, - supports_data: bool = False, ) -> None: if "links" not in nested_fields: return links_fields = nested_fields.pop("links") - link_edge_fields = set(DEFAULT_LINK_FIELDS) - if supports_data: - link_edge_fields.add("data") if isinstance(links_fields, dict): simple_fields = set(links_fields) - simple_variant = len(simple_fields - link_edge_fields) == 0 + diff = simple_fields - link_edge_fields + diff.discard("data") + simple_variant = len(diff) == 0 else: simple_variant = True simple_fields = link_edge_fields @@ -137,11 +134,7 @@ def product_types_query(fields): return query -def folders_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def folders_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("FoldersQuery") project_name_var = query.add_variable("projectName", "String!") folder_ids_var = query.add_variable("folderIds", "[String!]") @@ -181,11 +174,8 @@ def folders_graphql_query( folders_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) - add_links_fields( - folders_field, - nested_fields, - supports_data=links_support_data, - ) + + add_links_fields(folders_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -203,11 +193,7 @@ def folders_graphql_query( return query -def tasks_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def tasks_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("TasksQuery") project_name_var = query.add_variable("projectName", "String!") task_ids_var = query.add_variable("taskIds", "[String!]") @@ -237,11 +223,7 @@ def tasks_graphql_query( tasks_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) - add_links_fields( - tasks_field, - nested_fields, - supports_data=links_support_data, - ) + add_links_fields(tasks_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -259,11 +241,7 @@ def tasks_graphql_query( return query -def tasks_by_folder_paths_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def tasks_by_folder_paths_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("TasksByFolderPathQuery") project_name_var = query.add_variable("projectName", "String!") task_names_var = query.add_variable("taskNames", "[String!]") @@ -294,11 +272,8 @@ def tasks_by_folder_paths_graphql_query( tasks_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(fields) - add_links_fields( - tasks_field, - nested_fields, - supports_data=links_support_data, - ) + + add_links_fields(tasks_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -316,11 +291,7 @@ def tasks_by_folder_paths_graphql_query( return query -def products_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def products_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("ProductsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -352,11 +323,7 @@ def products_graphql_query( products_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields( - products_field, - nested_fields, - supports_data=links_support_data, - ) + add_links_fields(products_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -374,11 +341,7 @@ def products_graphql_query( return query -def versions_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def versions_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("VersionsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -411,11 +374,7 @@ def versions_graphql_query( versions_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields( - versions_field, - nested_fields, - supports_data=links_support_data, - ) + add_links_fields(versions_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -433,11 +392,7 @@ def versions_graphql_query( return query -def representations_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def representations_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("RepresentationsQuery") project_name_var = query.add_variable("projectName", "String!") @@ -468,11 +423,7 @@ def representations_graphql_query( repres_field.set_filter("filter", filter_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields( - repres_field, - nested_fields, - supports_data=links_support_data, - ) + add_links_fields(repres_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): @@ -589,11 +540,7 @@ def representations_hierarchy_qraphql_query( return query -def workfiles_info_graphql_query( - fields: set[str], - *, - links_support_data: bool = False, -) -> GraphQlQuery: +def workfiles_info_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("WorkfilesInfo") project_name_var = query.add_variable("projectName", "String!") workfiles_info_ids = query.add_variable("workfileIds", "[String!]") @@ -617,11 +564,7 @@ def workfiles_info_graphql_query( workfiles_field.set_filter("tags", tags_var) nested_fields = fields_to_dict(set(fields)) - add_links_fields( - workfiles_field, - nested_fields, - supports_data=links_support_data, - ) + add_links_fields(workfiles_field, nested_fields) query_queue = collections.deque() for key, value in nested_fields.items(): From 4e9bf62929c36842e6454d4eeb535aff835f9b97 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:45:14 +0100 Subject: [PATCH 358/506] remove f string --- 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 db5f49b1f..ab3a3d1f0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -2465,7 +2465,7 @@ def _prepare_link_fields(self, fields: set[str]) -> None: fields.add(f"links.{field}") if self.links_graphql_support_data(): - fields.add(f"links.data") + fields.add("links.data") def _prepare_advanced_filters( self, filters: Union[str, dict[str, Any], None] From 4b9eda40c94bd90c463d9fe57e18c20289299b95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:48:03 +0100 Subject: [PATCH 359/506] remove forgotten 'links_support_data' --- ayon_api/_api_helpers/links.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index de47107b5..dc73f7890 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -362,10 +362,7 @@ def get_entities_links( return output link_fields = {"id", "links"} - query = query_func( - link_fields, - links_support_data=self.links_graphql_support_data(), - ) + query = query_func(link_fields) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) From 0b11ca8e72585e2c78f4b2b62d4f8d63bcdd580f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 18 Mar 2026 16:50:23 +0100 Subject: [PATCH 360/506] fix get_entities_links --- ayon_api/_api_helpers/links.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index dc73f7890..b2b8d0973 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -362,6 +362,7 @@ def get_entities_links( return output link_fields = {"id", "links"} + self._prepare_link_fields(link_fields) query = query_func(link_fields) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) From 1581685e3f31070549c2c63d937f24a2bcffd437 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:51:33 +0100 Subject: [PATCH 361/506] modify CI actions to trigger PyPi upload --- .github/workflows/create_release.yml | 8 ++++++++ .github/workflows/python-publish.yml | 10 +++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 5aac8a0b9..89f9c1ea5 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -99,7 +99,15 @@ jobs: unprotect_reviews: true - name: Create GitHub Release + id: create_release uses: softprops/action-gh-release@v2 with: + token: ${{ secrets.YNPUT_BOT_TOKEN }} tag_name: ${{ steps.get_version.outputs.version }} generate_release_notes: true + + publish: + needs: release + uses: ./.github/workflows/python-publish.yml + secrets: + PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index c2808e961..6c282bcc9 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -6,11 +6,15 @@ # separate terms of service, privacy policy, and support # documentation. -name: ⬆️ Upload Python Package +name: ⬆️ Upload PyPi Package on: release: types: [published] + workflow_call: + secrets: + PYPI_API_TOKEN: + required: true permissions: contents: read @@ -23,9 +27,9 @@ jobs: url: https://pypi.org/p/ayon-python-api steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.10.x' - name: Install dependencies From b3846e6747663004d735cbd1141aa7d862b97725 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 19 Mar 2026 10:53:09 +0000 Subject: [PATCH 362/506] Release version 1.2.13 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index dfab06ada..baa8febc8 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.13-dev" +__version__ = "1.2.13" diff --git a/pyproject.toml b/pyproject.toml index 56a1c567d..50b8ebf1f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.13-dev" +version = "1.2.13" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.13-dev" +version = "1.2.13" description = "AYON Python API" authors = [ "ynput.io " From c7d176652ee1aaeaa557da7096fc3f067c52b145 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 19 Mar 2026 10:53:28 +0000 Subject: [PATCH 363/506] Bump version to 1.2.14-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index baa8febc8..6a68e5719 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.13" +__version__ = "1.2.14-dev" diff --git a/pyproject.toml b/pyproject.toml index 50b8ebf1f..1d0c16246 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.13" +version = "1.2.14-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.13" +version = "1.2.14-dev" description = "AYON Python API" authors = [ "ynput.io " From c94b0e76dceba52712d423e714719d4f5e96629b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:53:43 +0100 Subject: [PATCH 364/506] actually filter graphql project by name --- ayon_api/_api_helpers/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index a909100cf..97c61ec99 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -332,6 +332,7 @@ def get_project( graphql_project = next(self._get_graphql_projects( None, None, + project_name=project_name, fields=graphql_fields, own_attributes=own_attributes, ), None) From 25fd6033a426f80a327926623c9e8777a8893e15 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 19 Mar 2026 15:57:41 +0100 Subject: [PATCH 365/506] better merge of rest and graphql project data --- ayon_api/_api_helpers/projects.py | 45 ++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 97c61ec99..cc21a43ff 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -291,16 +291,14 @@ def get_projects( return projects_by_name = {p["name"]: p for p in projects} - for project in self.get_rest_projects(active, library): + for project in self.get_rest_projects(active=active, library=library): + if own_attributes: + fill_own_attribs(project) + name = project["name"] - graphql_p = projects_by_name.get(name) - if graphql_p: - for key in ( - "productTypes", - "usedTags", - ): - if key in graphql_p: - project[key] = graphql_p[key] + graphql_project = projects_by_name.get(name) + self._merge_project_graphql_data(project, graphql_project) + yield project def get_project( @@ -342,13 +340,9 @@ def get_project( project = self.get_rest_project(project_name) if own_attributes: fill_own_attribs(project) - if graphql_project: - for key in ( - "productTypes", - "usedTags", - ): - if key in graphql_project: - project[key] = graphql_project[key] + + self._merge_project_graphql_data(project, graphql_project) + return project def create_project( @@ -818,6 +812,25 @@ def _get_graphql_projects( self._fill_project_entity_data(project) yield project + def _merge_project_graphql_data( + self, + rest_project: dict[str, Any], + graphql_project: Optional[dict[str, Any]], + ) -> None: + if not graphql_project: + return + + for key, value in graphql_project.items(): + if ( + key not in rest_project + or key in ( + "productBaseTypes", + "productTypes", + "usedTags", + ) + ): + rest_project[key] = value + def _get_project_roots_values( self, project_name: str, From cc1ec04df6816f5451e3b7192a279b496b189b16 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Thu, 19 Mar 2026 21:26:15 -0400 Subject: [PATCH 366/506] Fix retries raise condition on upload/download files. --- ayon_api/server_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index ab3a3d1f0..a325bef47 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1419,7 +1419,7 @@ def _download_file_to_stream( requests.exceptions.Timeout, requests.exceptions.ConnectionError, ): - if attempt == retries: + if attempt == retries - 1: raise progress.next_attempt() @@ -1838,7 +1838,7 @@ def _upload_file( requests.exceptions.Timeout, requests.exceptions.ConnectionError, ): - if attempt == retries: + if attempt == retries - 1: raise progress.next_attempt() progress.reset_transferred() From 405bf0cf90cbd131e41a05269c9e5de8739c63f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:53:31 +0100 Subject: [PATCH 367/506] make sure all attributes are filled --- ayon_api/_api_helpers/projects.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index cc21a43ff..65513d7d7 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -162,6 +162,9 @@ def get_rest_project( if response.status != 200: return None project = response.data + attrib = project["attrib"] + for attr_name in self.get_attributes_for_type("project"): + attrib.setdefault(attr_name, None) self._fill_project_entity_data(project) return project From 9c4d08c9fd087db1f96c54b16338e14ccafe15f4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 20 Mar 2026 08:53:40 +0100 Subject: [PATCH 368/506] define method in base --- ayon_api/_api_helpers/base.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index 8ef198c68..cca8aa8e3 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -132,6 +132,11 @@ def get_user( ) -> Optional[dict[str, Any]]: raise NotImplementedError() + def get_attributes_for_type( + self, entity_type: AttributeScope + ) -> set[str]: + raise NotImplementedError() + def get_attributes_fields_for_type( self, entity_type: AttributeScope ) -> set[str]: From 7dda6d70afbb52e51e876bac3e0a38f24f81227c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 20 Mar 2026 12:48:07 +0100 Subject: [PATCH 369/506] Allow to retry on server response with status code 502 or 503 --- ayon_api/server_api.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a325bef47..6b007c26b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1228,6 +1228,20 @@ def _do_rest_request(self, function, url, **kwargs): for retry_idx in reversed(range(max_retries)): try: response = function(url, **kwargs) + + # Usually these mean, try later. + # 502: returned by the proxy: nginx + # 503: returned by the server: if no capacity + if response.status_code in {502, 503}: + new_response = RestApiResponse(response) + self.log.warning( + "Server returned %s status code." + " Retrying with longer delay...", + response.status_code + ) + if retry_idx != 0: + time.sleep(2) + continue break except ConnectionRefusedError: @@ -1269,7 +1283,8 @@ def _do_rest_request(self, function, url, **kwargs): } ) - time.sleep(0.1) + if retry_idx != 0: + time.sleep(0.1) if new_response is not None: return new_response From 114aab72d9041473f9bd876aca6529eacc23c8f3 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 20 Mar 2026 12:10:36 +0000 Subject: [PATCH 370/506] Release version 1.2.14 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 6a68e5719..ab1ff7a11 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.14-dev" +__version__ = "1.2.14" diff --git a/pyproject.toml b/pyproject.toml index 1d0c16246..89c4a4712 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.14-dev" +version = "1.2.14" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.14-dev" +version = "1.2.14" description = "AYON Python API" authors = [ "ynput.io " From e1716a6e5e83f5e35127a05db0523d44b1c84d18 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 20 Mar 2026 12:10:55 +0000 Subject: [PATCH 371/506] Bump version to 1.2.15-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index ab1ff7a11..3aeb258f3 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.14" +__version__ = "1.2.15-dev" diff --git a/pyproject.toml b/pyproject.toml index 89c4a4712..c82097173 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.14" +version = "1.2.15-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.14" +version = "1.2.15-dev" description = "AYON Python API" authors = [ "ynput.io " From a3b5877e46f7319e8ebb087c3b4bdd8844af765a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:49:00 +0200 Subject: [PATCH 372/506] fix handling of headers kwarg --- ayon_api/server_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6b007c26b..dafc9c92e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1792,7 +1792,10 @@ def _upload_file( url = self._endpoint_to_url(endpoint, use_rest=False) progress.set_destination_url(url) - headers = kwargs.setdefault("headers", {}) + headers = kwargs.get("headers") + if headers is None: + kwargs["headers"] = headers = {} + headers_keys_by_low_key = {key.lower(): key for key in headers} if self._session is None: for key, value in self.get_headers().items(): From b8b5fad9052ad5839fd8d3c7316f51e521c43f30 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:49:19 +0200 Subject: [PATCH 373/506] remove headers kwarg from upload reviewable --- ayon_api/_api.py | 3 --- ayon_api/server_api.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index a00bb04c0..249ffb07f 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1276,7 +1276,6 @@ def upload_reviewable( content_type: Optional[str] = None, filename: Optional[str] = None, progress: Optional[TransferProgress] = None, - headers: Optional[dict[str, Any]] = None, **kwargs, ) -> requests.Response: """Upload reviewable file to server. @@ -1291,7 +1290,6 @@ def upload_reviewable( filename (Optional[str]): User as original filename. Filename from 'filepath' is used when not filled. progress (Optional[TransferProgress]): Progress. - headers (Optional[dict[str, Any]]): Headers. Returns: requests.Response: Server response. @@ -1306,7 +1304,6 @@ def upload_reviewable( content_type=content_type, filename=filename, progress=progress, - headers=headers, **kwargs, ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index dafc9c92e..24613021e 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1985,7 +1985,6 @@ def upload_reviewable( content_type: Optional[str] = None, filename: Optional[str] = None, progress: Optional[TransferProgress] = None, - headers: Optional[dict[str, Any]] = None, **kwargs ) -> requests.Response: """Upload reviewable file to server. @@ -2000,7 +1999,6 @@ def upload_reviewable( filename (Optional[str]): User as original filename. Filename from 'filepath' is used when not filled. progress (Optional[TransferProgress]): Progress. - headers (Optional[dict[str, Any]]): Headers. Returns: requests.Response: Server response. @@ -2029,7 +2027,6 @@ def upload_reviewable( progress=progress, content_type=content_type, filename=filename, - headers=headers, request_type=RequestTypes.post, **kwargs ) From 929a1e79b5ea37565ad79561a7524e584645029c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 09:56:11 +0200 Subject: [PATCH 374/506] also handle 404 status for download --- 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 6b007c26b..6a5a0bc89 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1838,7 +1838,7 @@ def _upload_file( **kwargs ) # Auto-fix missing 'api/' - if response.status_code == 405 and not api_prepended: + if response.status_code in (404, 405) and not api_prepended: api_prepended = True if ( not endpoint.startswith(self._base_url) From f6f1cf889dceff53fd0aad99093c79a38720319c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:08:34 +0200 Subject: [PATCH 375/506] correct the statuses --- ayon_api/server_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6a5a0bc89..1d345f12d 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1408,7 +1408,7 @@ def _download_file_to_stream( try: with get_func(url, **kwargs) as response: # Auto-fix missing 'api/' - if response.status_code == 405 and not api_prepended: + if response.status_code == 404 and not api_prepended: api_prepended = True if ( not endpoint.startswith(self._base_url) @@ -1838,7 +1838,7 @@ def _upload_file( **kwargs ) # Auto-fix missing 'api/' - if response.status_code in (404, 405) and not api_prepended: + if response.status_code in 405 and not api_prepended: api_prepended = True if ( not endpoint.startswith(self._base_url) From b687aa1b067dcfe8ed91199bbaab765b4643a550 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:09:17 +0200 Subject: [PATCH 376/506] fix condition --- 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 1d345f12d..84816c388 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1838,7 +1838,7 @@ def _upload_file( **kwargs ) # Auto-fix missing 'api/' - if response.status_code in 405 and not api_prepended: + if response.status_code == 405 and not api_prepended: api_prepended = True if ( not endpoint.startswith(self._base_url) From 685f5650a77d009f3b1111b89b9ac0cff07078b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:15:17 +0200 Subject: [PATCH 377/506] added text filter and first or last filter options --- ayon_api/_api_helpers/events.py | 9 +++++++++ ayon_api/graphql_queries.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index 19e12c6b8..516f54617 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -40,6 +40,7 @@ def get_events( project_names: Optional[Iterable[str]] = None, statuses: Optional[Iterable[EventStatus]] = None, users: Optional[Iterable[str]] = None, + text_filter: Optional[str] = None, include_logs: Optional[bool] = None, has_children: Optional[bool] = None, newer_than: Optional[str] = None, @@ -47,6 +48,8 @@ def get_events( fields: Optional[Iterable[str]] = None, limit: Optional[int] = None, order: Optional[SortOrder] = None, + first: Optional[int] = None, + last: Optional[int] = None, states: Optional[Iterable[str]] = None, ) -> Generator[dict[str, Any], None, None]: """Get events from server with filtering options. @@ -62,6 +65,7 @@ def get_events( statuses (Optional[Iterable[EventStatus]]): Filtering by statuses. users (Optional[Iterable[str]]): Filtering by users who created/triggered an event. + text_filter (Optional[str]): Filtering by text in event payload. include_logs (Optional[bool]): Query also log events. has_children (Optional[bool]): Event is with/without children events. If 'None' then all events are returned, default. @@ -75,6 +79,8 @@ def get_events( order (Optional[SortOrder]): Order events in ascending or descending order. It is recommended to set 'limit' when used descending. + first (Optional[int]): Get first n events. + last (Optional[int]): Get last n events. states (Optional[Iterable[str]]): DEPRECATED Filtering by states. Use 'statuses' instead. @@ -111,6 +117,9 @@ def get_events( ("hasChildrenFilter", has_children), ("newerThanFilter", newer_than), ("olderThanFilter", older_than), + ("textFilter", text_filter), + ("firstFilter", first), + ("lastFilter", last), ): if filter_value is not None: filters[filter_key] = filter_value diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 27719d389..6d299c674 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -593,6 +593,9 @@ def events_graphql_query(fields, order, use_states=False): has_children_var = query.add_variable("hasChildrenFilter", "Boolean!") newer_than_var = query.add_variable("newerThanFilter", "String!") older_than_var = query.add_variable("olderThanFilter", "String!") + text_filter_var = query.add_variable("textFilter", "String!") + first_n_var = query.add_variable("firstFilter", "Int!") + last_n_var = query.add_variable("lastFilter", "Int!") statuses_filter_name = "statuses" if use_states: @@ -607,6 +610,9 @@ def events_graphql_query(fields, order, use_states=False): events_field.set_filter("hasChildren", has_children_var) events_field.set_filter("newerThan", newer_than_var) events_field.set_filter("olderThan", older_than_var) + events_field.set_filter("filter", text_filter_var) + events_field.set_filter("first", first_n_var) + events_field.set_filter("last", last_n_var) nested_fields = fields_to_dict(set(fields)) From db978b379e03a971997e237b78b61da85752bf51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:28:46 +0200 Subject: [PATCH 378/506] fix library filtering --- ayon_api/_api_helpers/projects.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 65513d7d7..86e003606 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -810,6 +810,10 @@ def _get_graphql_projects( for project in parsed_data["projects"]: if active is not None and active is not project["active"]: continue + + if library is not None and library is not project["library"]: + continue + if own_attributes: fill_own_attribs(project) self._fill_project_entity_data(project) From c07c2a3aeb29d239e6f295d205d7e5c3da599acb Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:56:08 +0200 Subject: [PATCH 379/506] fix fill of own attrib for project --- ayon_api/_api_helpers/projects.py | 18 ++++++++++++++++-- ayon_api/server_api.py | 7 +++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 65513d7d7..d583cf62e 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -1,5 +1,6 @@ from __future__ import annotations +import copy import json import platform import warnings @@ -810,8 +811,21 @@ def _get_graphql_projects( for project in parsed_data["projects"]: if active is not None and active is not project["active"]: continue - if own_attributes: - fill_own_attribs(project) + + all_attrib = project.get("allAttrib") + if isinstance(all_attrib, str): + all_attrib = json.loads(all_attrib) + project["allAttrib"] = all_attrib + + if own_attributes and all_attrib: + own_attrib = {} + if all_attrib: + own_attrib = copy.deepcopy(all_attrib) + attrib = project.get("attrib", {}) + for key in attrib.keys(): + own_attrib.setdefault(key, None) + project["ownAttrib"] = own_attrib + self._fill_project_entity_data(project) yield project diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 6b007c26b..8a730b907 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -2425,8 +2425,11 @@ def _prepare_fields( fields.remove("attrib") fields |= self.get_attributes_fields_for_type(entity_type) - if own_attributes and entity_type in {"project", "folder", "task"}: - fields.add("ownAttrib") + if own_attributes: + if entity_type == "project": + fields.add("allAttrib") + elif entity_type in {"folder", "task"}: + fields.add("ownAttrib") if entity_type != "project": return From cfd51f022e95e77f863af03eb8bb691ef4511a51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 10:56:48 +0200 Subject: [PATCH 380/506] update public api --- ayon_api/_api.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index a00bb04c0..f323a82ca 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -3248,6 +3248,7 @@ def get_events( project_names: Optional[Iterable[str]] = None, statuses: Optional[Iterable[EventStatus]] = None, users: Optional[Iterable[str]] = None, + text_filter: Optional[str] = None, include_logs: Optional[bool] = None, has_children: Optional[bool] = None, newer_than: Optional[str] = None, @@ -3255,6 +3256,8 @@ def get_events( fields: Optional[Iterable[str]] = None, limit: Optional[int] = None, order: Optional[SortOrder] = None, + first: Optional[int] = None, + last: Optional[int] = None, states: Optional[Iterable[str]] = None, ) -> Generator[dict[str, Any], None, None]: """Get events from server with filtering options. @@ -3270,6 +3273,7 @@ def get_events( statuses (Optional[Iterable[EventStatus]]): Filtering by statuses. users (Optional[Iterable[str]]): Filtering by users who created/triggered an event. + text_filter (Optional[str]): Filtering by text in event payload. include_logs (Optional[bool]): Query also log events. has_children (Optional[bool]): Event is with/without children events. If 'None' then all events are returned, default. @@ -3283,6 +3287,8 @@ def get_events( order (Optional[SortOrder]): Order events in ascending or descending order. It is recommended to set 'limit' when used descending. + first (Optional[int]): Get first n events. + last (Optional[int]): Get last n events. states (Optional[Iterable[str]]): DEPRECATED Filtering by states. Use 'statuses' instead. @@ -3297,6 +3303,7 @@ def get_events( project_names=project_names, statuses=statuses, users=users, + text_filter=text_filter, include_logs=include_logs, has_children=has_children, newer_than=newer_than, @@ -3304,6 +3311,8 @@ def get_events( fields=fields, limit=limit, order=order, + first=first, + last=last, states=states, ) From 921294cc37cb7d1c49f92d2d67717582c9198a16 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 13:58:24 +0200 Subject: [PATCH 381/506] check for both statuses in both cases --- ayon_api/server_api.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index fda8f1aad..b4287e625 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1408,7 +1408,10 @@ def _download_file_to_stream( try: with get_func(url, **kwargs) as response: # Auto-fix missing 'api/' - if response.status_code == 404 and not api_prepended: + if ( + response.status_code in (404, 405) + and not api_prepended + ): api_prepended = True if ( not endpoint.startswith(self._base_url) @@ -1841,7 +1844,7 @@ def _upload_file( **kwargs ) # Auto-fix missing 'api/' - if response.status_code == 405 and not api_prepended: + if response.status_code in (404, 405) and not api_prepended: api_prepended = True if ( not endpoint.startswith(self._base_url) From f0d7d21a5213c80458104c367886a4a4150a8941 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:21:14 +0200 Subject: [PATCH 382/506] fix first and last fitlers --- ayon_api/_api_helpers/events.py | 10 ++++++++-- ayon_api/graphql_queries.py | 4 ---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/ayon_api/_api_helpers/events.py b/ayon_api/_api_helpers/events.py index 516f54617..03aaf77d2 100644 --- a/ayon_api/_api_helpers/events.py +++ b/ayon_api/_api_helpers/events.py @@ -118,8 +118,6 @@ def get_events( ("newerThanFilter", newer_than), ("olderThanFilter", older_than), ("textFilter", text_filter), - ("firstFilter", first), - ("lastFilter", last), ): if filter_value is not None: filters[filter_key] = filter_value @@ -134,6 +132,14 @@ def get_events( for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) + events_field = query.get_field_by_path("events") + if last is not None: + events_field.set_limit(last) + events_field.set_order(SortOrder.descending) + elif first is not None: + events_field.set_limit(first) + events_field.set_order(SortOrder.ascending) + if limit: events_field = query.get_field_by_path("events") events_field.set_limit(limit) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 6d299c674..100c0aa9d 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -594,8 +594,6 @@ def events_graphql_query(fields, order, use_states=False): newer_than_var = query.add_variable("newerThanFilter", "String!") older_than_var = query.add_variable("olderThanFilter", "String!") text_filter_var = query.add_variable("textFilter", "String!") - first_n_var = query.add_variable("firstFilter", "Int!") - last_n_var = query.add_variable("lastFilter", "Int!") statuses_filter_name = "statuses" if use_states: @@ -611,8 +609,6 @@ def events_graphql_query(fields, order, use_states=False): events_field.set_filter("newerThan", newer_than_var) events_field.set_filter("olderThan", older_than_var) events_field.set_filter("filter", text_filter_var) - events_field.set_filter("first", first_n_var) - events_field.set_filter("last", last_n_var) nested_fields = fields_to_dict(set(fields)) From 1eb1e5f7392027e1bab9a5d5c5b649742192cc5d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 1 Apr 2026 12:24:13 +0000 Subject: [PATCH 383/506] Release version 1.2.15 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 3aeb258f3..e087fb87b 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.15-dev" +__version__ = "1.2.15" diff --git a/pyproject.toml b/pyproject.toml index c82097173..47d1424fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.15-dev" +version = "1.2.15" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.15-dev" +version = "1.2.15" description = "AYON Python API" authors = [ "ynput.io " From cabc36a3dafa8412afba982088757fd5a3a75455 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 1 Apr 2026 12:24:32 +0000 Subject: [PATCH 384/506] Bump version to 1.2.16-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index e087fb87b..c7ad5ef65 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.15" +__version__ = "1.2.16-dev" diff --git a/pyproject.toml b/pyproject.toml index 47d1424fb..d3c5fdeb1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.15" +version = "1.2.16-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.15" +version = "1.2.16-dev" description = "AYON Python API" authors = [ "ynput.io " From d447e9be34f3a4b74276ab554070251bd83bdfce Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:16:36 +0200 Subject: [PATCH 385/506] fix type hint --- ayon_api/_api_helpers/attributes.py | 3 +-- ayon_api/_api_helpers/base.py | 3 ++- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py index 26db47484..f574322cb 100644 --- a/ayon_api/_api_helpers/attributes.py +++ b/ayon_api/_api_helpers/attributes.py @@ -9,7 +9,6 @@ if typing.TYPE_CHECKING: from ayon_api.typing import ( AttributeSchemaDataDict, - AttributeSchemaDict, AttributesSchemaDict, AttributeScope, ) @@ -92,7 +91,7 @@ def remove_attribute_config(self, attribute_name: str) -> None: def get_attributes_for_type( self, entity_type: AttributeScope - ) -> dict[str, AttributeSchemaDict]: + ) -> dict[str, AttributeSchemaDataDict]: """Get attribute schemas available for an entity type. Example:: diff --git a/ayon_api/_api_helpers/base.py b/ayon_api/_api_helpers/base.py index cca8aa8e3..d9284207b 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -15,6 +15,7 @@ ProjectDict, StreamType, AttributeScope, + AttributeSchemaDataDict, ) _PLACEHOLDER = object() @@ -134,7 +135,7 @@ def get_user( def get_attributes_for_type( self, entity_type: AttributeScope - ) -> set[str]: + ) -> dict[str, AttributeSchemaDataDict]: raise NotImplementedError() def get_attributes_fields_for_type( From fd470385ac7b5320e6200500931e2f98394d997e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:17:03 +0200 Subject: [PATCH 386/506] use allAttrib in project --- ayon_api/_api_helpers/projects.py | 36 ++++++++++++++++++------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 70099d29b..9aeeb5f65 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -663,7 +663,7 @@ def _get_project_graphql_fields( if fields is None: return set(), ProjectFetchType.REST - rest_list_fields = { + rest_fields = { "name", "code", "active", @@ -671,10 +671,11 @@ def _get_project_graphql_fields( "updatedAt", } graphql_fields = set() - if len(fields - rest_list_fields) == 0: + if len(fields - rest_fields) == 0: return graphql_fields, ProjectFetchType.RESTList must_use_graphql = False + add_all_attrib = False for field in tuple(fields): # Product types are available only in GraphQl if field == "usedTags": @@ -707,11 +708,9 @@ def _get_project_graphql_fields( elif field.startswith("bundle"): graphql_fields.add(field) - elif field == "attrib": - fields.discard("attrib") - graphql_fields |= self.get_attributes_fields_for_type( - "project" - ) + elif field == "attrib" or field.startswith("attrib."): + fields.discard(field) + add_all_attrib = True # NOTE 'config' in GraphQl is NOT the same as from REST api. # - At the moment of this comment there is missing 'productBaseTypes'. @@ -726,6 +725,8 @@ def _get_project_graphql_fields( remainders = fields - (inters | graphql_fields) if not remainders: graphql_fields |= inters + if add_all_attrib: + graphql_fields.add("allAttrib") return graphql_fields, ProjectFetchType.GraphQl if must_use_graphql: @@ -820,14 +821,19 @@ def _get_graphql_projects( all_attrib = json.loads(all_attrib) project["allAttrib"] = all_attrib - if own_attributes and all_attrib: - own_attrib = {} - if all_attrib: - own_attrib = copy.deepcopy(all_attrib) - attrib = project.get("attrib", {}) - for key in attrib.keys(): - own_attrib.setdefault(key, None) - project["ownAttrib"] = own_attrib + if all_attrib is not None: + project["ownAttrib"] = list(all_attrib) + + attrib = copy.deepcopy(all_attrib) + project["attrib"] = attrib + for name, attr_data in ( + self.get_attributes_for_type("project").items() + ): + # NOTE 'default' can be 'None' + attrib.setdefault(name, attr_data["default"]) + + if own_attributes: + fill_own_attribs(project) self._fill_project_entity_data(project) yield project From f51075f264e5833e63dc5385559a0f12c7b31f0a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 11:31:35 +0200 Subject: [PATCH 387/506] use allAttrib instead of attrib --- ayon_api/_api_helpers/projects.py | 12 ++++++------ ayon_api/server_api.py | 30 +++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 9aeeb5f65..18350b7f7 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -816,15 +816,15 @@ def _get_graphql_projects( if library is not None and library is not project["library"]: continue + attrib = None all_attrib = project.get("allAttrib") if isinstance(all_attrib, str): - all_attrib = json.loads(all_attrib) - project["allAttrib"] = all_attrib + attrib = json.loads(all_attrib) - if all_attrib is not None: - project["ownAttrib"] = list(all_attrib) - - attrib = copy.deepcopy(all_attrib) + if attrib is not None: + # NOTE 'ownAttrib' logic might change in the future if + # allAttrib would return all attribute values. + project["ownAttrib"] = list(attrib) project["attrib"] = attrib for name, attr_data in ( self.get_attributes_for_type("project").items() diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 1b4668218..25c40203c 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -2424,16 +2424,21 @@ def _prepare_fields( if not fields: return - if "attrib" in fields: - fields.remove("attrib") - fields |= self.get_attributes_fields_for_type(entity_type) + add_all_attrib = False + for field in tuple(fields): + if field == "attrib" or field.startswith("attrib."): + fields.discard(field) + add_all_attrib = True if own_attributes: if entity_type == "project": - fields.add("allAttrib") + add_all_attrib = True elif entity_type in {"folder", "task"}: fields.add("ownAttrib") + if add_all_attrib: + fields.add("allAttrib") + if entity_type != "project": return @@ -2499,11 +2504,18 @@ def _prepare_advanced_filters( return filters def _convert_entity_data(self, entity: AnyEntityDict): - if not entity or "data" not in entity: + if not entity: return - entity_data = entity["data"] or {} - if isinstance(entity_data, str): - entity_data = json.loads(entity_data) + if "data" in entity: + entity_data = entity["data"] or {} + if isinstance(entity_data, str): + entity_data = json.loads(entity_data) + + entity["data"] = entity_data - entity["data"] = entity_data + all_attrib = entity.get("allAttrib") + if isinstance(all_attrib, str): + # NOTE: This expects server returns all attributes available for + # the entity type. + entity["attrib"] = json.loads(all_attrib) From 2dcce7b35279699b11e4e813a8db35954f5ee64e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:02:59 +0200 Subject: [PATCH 388/506] use similar approach using defaults in lists too --- ayon_api/_api_helpers/lists.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index df22e3844..d97fb1d5f 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -51,16 +51,17 @@ def get_entity_lists( if fields is None: fields = self.get_default_fields_for_type("entityList") - # List does not have 'attrib' field but has 'allAttrib' field - # which is json string and contains only values that are set o_fields = tuple(fields) fields = set() requires_attrib = False for field in o_fields: if field == "attrib" or field.startswith("attrib."): requires_attrib = True - field = "allAttrib" - fields.add(field) + else: + fields.add(field) + + if requires_attrib: + fields.add("allAttrib") if "items" in fields: fields.discard("items") @@ -71,7 +72,7 @@ def get_entity_lists( "items.position", } - available_attribs = [] + available_attribs = {} if requires_attrib: available_attribs = self.get_attributes_for_type("list") @@ -98,13 +99,11 @@ def get_entity_lists( entity_list["attributes"] = json.loads(attributes) if requires_attrib: - all_attrib = json.loads( - entity_list.get("allAttrib") or "{}" - ) - entity_list["attrib"] = { - attrib_name: all_attrib.get(attrib_name) - for attrib_name in available_attribs - } + attrib = json.loads(entity_list["allAttrib"]) + for attrib_name, attrib_data in available_attribs.items(): + attrib.setdefault(attrib_name, attrib_data["default"]) + + entity_list["attrib"] = attrib self._convert_entity_data(entity_list) From f58ba84269fee6e5f47da510f699fd23a09d1f7c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:15:47 +0200 Subject: [PATCH 389/506] add 'allAttrib' earlier --- ayon_api/_api_helpers/projects.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 18350b7f7..7430f28f1 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -712,6 +712,9 @@ def _get_project_graphql_fields( fields.discard(field) add_all_attrib = True + if add_all_attrib: + graphql_fields.add("allAttrib") + # NOTE 'config' in GraphQl is NOT the same as from REST api. # - At the moment of this comment there is missing 'productBaseTypes'. inters = fields & { @@ -725,8 +728,6 @@ def _get_project_graphql_fields( remainders = fields - (inters | graphql_fields) if not remainders: graphql_fields |= inters - if add_all_attrib: - graphql_fields.add("allAttrib") return graphql_fields, ProjectFetchType.GraphQl if must_use_graphql: From cc2e099e2bae577fc65d3a374bcc2e4241664f74 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:57:06 +0200 Subject: [PATCH 390/506] mark 'get_attributes_fields_for_type' as deprecated --- ayon_api/_api_helpers/attributes.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py index f574322cb..c9682e4d8 100644 --- a/ayon_api/_api_helpers/attributes.py +++ b/ayon_api/_api_helpers/attributes.py @@ -147,10 +147,18 @@ def get_attributes_fields_for_type( ) -> set[str]: """Prepare attribute fields for entity type. + DEPRECATED: Field 'attrib' is marked as deprecated and should not be + used for GraphQL queries. + Returns: set[str]: Attributes fields for entity type. """ + self.log.warning( + "Method 'get_attributes_fields_for_type' is deprecated and should" + " not be used for GraphQL queries. Use 'allAttrib' field instead" + " of 'attrib'." + ) attributes = self.get_attributes_for_type(entity_type) return { f"attrib.{attr}" From 1f1c2527b397d97aa712f3dc96062429c2720787 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:57:43 +0200 Subject: [PATCH 391/506] fix few issues with attributes --- ayon_api/_api_helpers/lists.py | 18 ++++----- ayon_api/_api_helpers/projects.py | 14 ++++--- ayon_api/constants.py | 4 -- ayon_api/server_api.py | 67 ++++++++++++++++++------------- 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index d97fb1d5f..6bab56f91 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -53,14 +53,14 @@ def get_entity_lists( o_fields = tuple(fields) fields = set() - requires_attrib = False + add_all_attrib = False for field in o_fields: if field == "attrib" or field.startswith("attrib."): - requires_attrib = True + add_all_attrib = True else: fields.add(field) - if requires_attrib: + if add_all_attrib: fields.add("allAttrib") if "items" in fields: @@ -73,7 +73,7 @@ def get_entity_lists( } available_attribs = {} - if requires_attrib: + if "allAttrib" in fields: available_attribs = self.get_attributes_for_type("list") if active is not None: @@ -98,15 +98,13 @@ def get_entity_lists( if isinstance(attributes, str): entity_list["attributes"] = json.loads(attributes) - if requires_attrib: - attrib = json.loads(entity_list["allAttrib"]) + self._convert_entity_data(entity_list) + + attrib = entity_list.get("attrib") + if attrib is not None: for attrib_name, attrib_data in available_attribs.items(): attrib.setdefault(attrib_name, attrib_data["default"]) - entity_list["attrib"] = attrib - - self._convert_entity_data(entity_list) - yield entity_list def get_entity_list_rest( diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 7430f28f1..b489c5a53 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -164,8 +164,10 @@ def get_rest_project( return None project = response.data attrib = project["attrib"] - for attr_name in self.get_attributes_for_type("project"): - attrib.setdefault(attr_name, None) + for attr_name, attr_data in ( + self.get_attributes_for_type("project").items() + ): + attrib.setdefault(attr_name, attr_data["default"]) self._fill_project_entity_data(project) return project @@ -809,6 +811,10 @@ def _get_graphql_projects( if project_name is not None: query.set_variable_value("projectName", project_name) + attributes = {} + if "allAttrib" in fields: + attributes = self.get_attributes_for_type("project") + for parsed_data in query.continuous_query(self): for project in parsed_data["projects"]: if active is not None and active is not project["active"]: @@ -827,9 +833,7 @@ def _get_graphql_projects( # allAttrib would return all attribute values. project["ownAttrib"] = list(attrib) project["attrib"] = attrib - for name, attr_data in ( - self.get_attributes_for_type("project").items() - ): + for name, attr_data in attributes.items(): # NOTE 'default' can be 'None' attrib.setdefault(name, attr_data["default"]) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index e39fc175f..c122e4eef 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -33,9 +33,6 @@ "hasPassword", "updatedAt", "apiKeyPreview", - "attrib.avatarUrl", - "attrib.email", - "attrib.fullName", } # --- Project folder types --- @@ -104,7 +101,6 @@ "linkTypes", "statuses", "tags", - "attrib", } # --- Folders --- diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 25c40203c..23331f1f7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -5,6 +5,7 @@ """ from __future__ import annotations +import copy import os import re import io @@ -1007,29 +1008,44 @@ def get_users( if not fields: fields = self.get_default_fields_for_type("user") + else: + fields = set(fields) + add_all_attrib = False + for field in tuple(fields): + if field == "attrib" or field.startswith("attrib."): + fields.discard(field) + add_all_attrib = True + + if add_all_attrib: + fields.add("allAttrib") - query = users_graphql_query(set(fields)) + query = users_graphql_query(fields) for attr, filter_value in filters.items(): query.set_variable_value(attr, filter_value) - attributes = self.get_attributes_for_type("user") + attributes = {} + if "allAttrib" in fields: + attributes = self.get_attributes_for_type("user") + for parsed_data in query.continuous_query(self): for user in parsed_data["users"]: access_groups = user.get("accessGroups") if isinstance(access_groups, str): user["accessGroups"] = json.loads(access_groups) - all_attrib = user.get("allAttrib") - if isinstance(all_attrib, str): - user["allAttrib"] = json.loads(all_attrib) - if "attrib" in user: - user["ownAttrib"] = user["attrib"].copy() - attrib = user["attrib"] - for key, value in tuple(attrib.items()): - if value is not None: - continue - attr_def = attributes.get(key) - if attr_def is not None: - attrib[key] = attr_def["default"] + + attrib = user.get("allAttrib") + if isinstance(attrib, str): + attrib = json.loads(attrib) + + if attrib is not None: + own_attrib = copy.deepcopy(attrib) + user["ownAttrib"] = own_attrib + for name, attr_data in attributes.items(): + attrib.setdefault(name, attr_data["default"]) + own_attrib.setdefault(name, None) + + user["attrib"] = attrib + yield user def get_user_by_name( @@ -1089,10 +1105,9 @@ def get_user( response.raise_for_status() user = response.data - # NOTE Server does return only filled attributes right now. - # This would fill all missing attributes with 'None'. - # for attr_name in self.get_attributes_for_type("user"): - # user["attrib"].setdefault(attr_name, None) + attributes = self.get_attributes_for_type("user") + for attr_name, attr_data in attributes.items(): + user["attrib"].setdefault(attr_name, attr_data["default"]) fill_own_attribs(user) return user @@ -2124,6 +2139,9 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: if entity_type == "activity": return set(DEFAULT_ACTIVITY_FIELDS) + if entity_type == "productType": + return set(DEFAULT_PRODUCT_TYPE_FIELDS) + if entity_type == "project": entity_type_defaults = set(DEFAULT_PROJECT_FIELDS) maj_v, min_v, patch_v, _, _ = self.server_version_tuple @@ -2154,9 +2172,6 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: if not self.graphql_allows_traits_in_representations: entity_type_defaults.discard("traits") - elif entity_type == "productType": - entity_type_defaults = set(DEFAULT_PRODUCT_TYPE_FIELDS) - elif entity_type == "workfile": entity_type_defaults = set(DEFAULT_WORKFILE_INFO_FIELDS) @@ -2165,15 +2180,13 @@ def get_default_fields_for_type(self, entity_type: str) -> set[str]: elif entity_type == "entityList": entity_type_defaults = set(DEFAULT_ENTITY_LIST_FIELDS) - # Attributes scope is 'list' - entity_type = "list" else: raise ValueError(f"Unknown entity type \"{entity_type}\"") - return ( - entity_type_defaults - | self.get_attributes_fields_for_type(entity_type) - ) + + entity_type_defaults.add("allAttrib") + + return entity_type_defaults def get_rest_entity_by_id( self, From 436614210a4a1f0a521a8b4f29155cdf65d9cb4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:02:03 +0200 Subject: [PATCH 392/506] reuse existing method --- ayon_api/server_api.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 23331f1f7..7ad2f5ba0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1010,14 +1010,7 @@ def get_users( fields = self.get_default_fields_for_type("user") else: fields = set(fields) - add_all_attrib = False - for field in tuple(fields): - if field == "attrib" or field.startswith("attrib."): - fields.discard(field) - add_all_attrib = True - - if add_all_attrib: - fields.add("allAttrib") + self._prepare_fields("user", fields) query = users_graphql_query(fields) for attr, filter_value in filters.items(): From af880577e0f3072fca0d62353e22501e570fcb4e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:42:24 +0200 Subject: [PATCH 393/506] remove unused import --- ayon_api/_api_helpers/projects.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index b489c5a53..58857248e 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -1,6 +1,5 @@ from __future__ import annotations -import copy import json import platform import warnings From f3bda4190044208002481dabdb5a10f463b35972 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:42:59 +0200 Subject: [PATCH 394/506] better handling of cached attributes --- ayon_api/_api_helpers/attributes.py | 118 +++++++++++++++++++++------- 1 file changed, 90 insertions(+), 28 deletions(-) diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py index c9682e4d8..c9deb6356 100644 --- a/ayon_api/_api_helpers/attributes.py +++ b/ayon_api/_api_helpers/attributes.py @@ -1,38 +1,107 @@ from __future__ import annotations +import copy +import time import typing from typing import Optional -import copy from .base import BaseServerAPI if typing.TYPE_CHECKING: from ayon_api.typing import ( + AttributeSchemaDict, AttributeSchemaDataDict, AttributesSchemaDict, AttributeScope, ) +class _AttributesCache: + _schema = None + _last_fetch = 0 + _timeout = 60 + _attributes_by_type = {} + + def reset_schema(self) -> None: + self._schema = None + self._last_fetch = 0 + self._attributes_by_type = {} + + def set_timeout(self, timeout: int) -> None: + self._timeout = timeout + + def get_schema(self) -> AttributesSchemaDict: + return copy.deepcopy(self._schema) + + def set_schema(self, schema: AttributesSchemaDict) -> None: + self._schema = schema + self._last_fetch = time.time() + + def is_valid(self) -> bool: + if self._schema is None: + return False + return time.time() - self._last_fetch < self._timeout + + def invalidate(self) -> None: + if not self.is_valid(): + self.reset_schema() + + def get_attributes_for_type( + self, entity_type: AttributeScope + ) -> list[AttributeSchemaDict]: + attributes = self._attributes_by_type.get(entity_type) + if attributes is not None: + return attributes + + attributes_schema = self.get_schema() + if attributes_schema is None: + raise ValueError("Attributes schema is not cached.") + + attributes = [] + for attr in attributes_schema["attributes"]: + if entity_type not in attr["scope"]: + continue + attributes.append(attr) + + self._attributes_by_type[entity_type] = attributes + return attributes + + class AttributesAPI(BaseServerAPI): - _attributes_schema = None - _entity_type_attributes_cache = {} + _attributes_cache = _AttributesCache() def get_attributes_schema( self, use_cache: bool = True ) -> AttributesSchemaDict: if not use_cache: - self.reset_attributes_schema() + self._attributes_cache.reset_schema() + else: + self._attributes_cache.invalidate() - if self._attributes_schema is None: + if not self._attributes_cache.is_valid(): result = self.get("attributes") result.raise_for_status() - self._attributes_schema = result.data - return copy.deepcopy(self._attributes_schema) + self._attributes_cache.set_schema(result.data) + return self._attributes_cache.get_schema() def reset_attributes_schema(self) -> None: - self._attributes_schema = None - self._entity_type_attributes_cache = {} + """Reset attributes schema cache. + + DEPRECATED: + Use 'reset_attributes_cache' instead. + + """ + self.log.warning( + "Used deprecated function 'reset_attributes_schema'." + " Please use 'reset_attributes_cache' instead." + ) + self.reset_attributes_cache() + + def reset_attributes_cache(self) -> None: + self._attributes_cache.reset_schema() + + def set_attributes_cache_timeout(self, timeout: int) -> None: + self._attributes_cache.set_timeout(timeout) def set_attribute_config( self, @@ -63,12 +132,10 @@ def set_attribute_config( position=position, builtin=builtin ) - if response.status_code != 204: - # TODO raise different exception - raise ValueError( - f"Attribute \"{attribute_name}\" was not created/updated." - f" {response.detail}" - ) + response.raise_for_status( + f"Attribute \"{attribute_name}\" was not created/updated." + f" {response.detail}" + ) self.reset_attributes_schema() @@ -128,19 +195,14 @@ def get_attributes_for_type( for entered entity type. """ - attributes = self._entity_type_attributes_cache.get(entity_type) - if attributes is None: - attributes_schema = self.get_attributes_schema() - attributes = {} - for attr in attributes_schema["attributes"]: - if entity_type not in attr["scope"]: - continue - attr_name = attr["name"] - attributes[attr_name] = attr["data"] - - self._entity_type_attributes_cache[entity_type] = attributes - - return copy.deepcopy(attributes) + # Make sure attributes are cached + self.get_attributes_schema() + return { + attr["name"]: attr["data"] + for attr in self._attributes_cache.get_attributes_for_type( + entity_type + ) + } def get_attributes_fields_for_type( self, entity_type: AttributeScope From d9649c564132a6b768c50a67b09dd83faaea0d1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:44:07 +0200 Subject: [PATCH 395/506] update public api --- ayon_api/__init__.py | 4 ++++ ayon_api/_api.py | 25 ++++++++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index fc89a1dfb..c964c24ef 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -148,6 +148,8 @@ enroll_event_job, get_attributes_schema, reset_attributes_schema, + reset_attributes_cache, + set_attributes_cache_timeout, set_attribute_config, remove_attribute_config, get_attributes_for_type, @@ -433,6 +435,8 @@ "enroll_event_job", "get_attributes_schema", "reset_attributes_schema", + "reset_attributes_cache", + "set_attributes_cache_timeout", "set_attribute_config", "remove_attribute_config", "get_attributes_for_type", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index d18d00e74..fb421c12e 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -3582,10 +3582,30 @@ def get_attributes_schema( def reset_attributes_schema() -> None: + """Reset attributes schema cache. + + DEPRECATED: + Use 'reset_attributes_cache' instead. + + """ con = get_server_api_connection() return con.reset_attributes_schema() +def reset_attributes_cache() -> None: + con = get_server_api_connection() + return con.reset_attributes_cache() + + +def set_attributes_cache_timeout( + timeout: int, +) -> None: + con = get_server_api_connection() + return con.set_attributes_cache_timeout( + timeout=timeout, + ) + + def set_attribute_config( attribute_name: str, data: AttributeSchemaDataDict, @@ -3622,7 +3642,7 @@ def remove_attribute_config( def get_attributes_for_type( entity_type: AttributeScope, -) -> dict[str, AttributeSchemaDict]: +) -> dict[str, AttributeSchemaDataDict]: """Get attribute schemas available for an entity type. Example:: @@ -3670,6 +3690,9 @@ def get_attributes_fields_for_type( ) -> set[str]: """Prepare attribute fields for entity type. + DEPRECATED: Field 'attrib' is marked as deprecated and should not be + used for GraphQL queries. + Returns: set[str]: Attributes fields for entity type. From 5bc86186f48408330ab74b0eae9707abc824ba7f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:59:49 +0200 Subject: [PATCH 396/506] remove unused import --- ayon_api/_api.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index fb421c12e..87437368f 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -55,7 +55,6 @@ EnrollEventData, AttributeScope, AttributeSchemaDataDict, - AttributeSchemaDict, AttributesSchemaDict, AddonsInfoDict, InstallersInfoDict, From 8bb63f70e97d3534be019605ec06c046f6c18249 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 7 Apr 2026 09:33:58 +0000 Subject: [PATCH 397/506] Release version 1.2.16 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index c7ad5ef65..7f8fd45a2 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.16-dev" +__version__ = "1.2.16" diff --git a/pyproject.toml b/pyproject.toml index d3c5fdeb1..1395edaa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.16-dev" +version = "1.2.16" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.16-dev" +version = "1.2.16" description = "AYON Python API" authors = [ "ynput.io " From 4686d03a174588be09254fe56dd533c7d10b1c61 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 7 Apr 2026 09:34:20 +0000 Subject: [PATCH 398/506] Bump version to 1.2.17-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 7f8fd45a2..a19f2ace4 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.16" +__version__ = "1.2.17-dev" diff --git a/pyproject.toml b/pyproject.toml index 1395edaa8..a131446fc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.16" +version = "1.2.17-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.16" +version = "1.2.17-dev" description = "AYON Python API" authors = [ "ynput.io " From 68fc5b6e04b6e456c0487062ba68c89f421dc5b7 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 15 Apr 2026 14:30:29 -0400 Subject: [PATCH 399/506] Fix create_entity_list_item missing entityId. --- ayon_api/_api_helpers/lists.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 6bab56f91..99a71dd04 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -342,9 +342,11 @@ def create_entity_list_item( """ if item_id is None: item_id = create_entity_id() + + data = data or {} kwargs = { "id": item_id, - "entityId": list_id, + "entityId": data.pop("entityId"), } for key, value in ( ("position", position), From 4e3c1e5977971f20560b4b05c910d08c351b7e5e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Apr 2026 09:56:31 +0200 Subject: [PATCH 400/506] proper fix of the function --- ayon_api/_api.py | 3 +++ ayon_api/_api_helpers/lists.py | 5 +++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 87437368f..98e794203 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -7710,6 +7710,7 @@ def set_entity_list_attribute_definitions( def create_entity_list_item( project_name: str, list_id: str, + entity_id: str, *, position: Optional[int] = None, label: Optional[str] = None, @@ -7723,6 +7724,7 @@ def create_entity_list_item( Args: project_name (str): Project name where entity list lives. list_id (str): Entity list id where item will be added. + entity_id (str): Id of entity added to the list. position (Optional[int]): Position of item in entity list. label (Optional[str]): Label of item in entity list. attrib (Optional[dict[str, Any]]): Item attribute values. @@ -7738,6 +7740,7 @@ def create_entity_list_item( return con.create_entity_list_item( project_name=project_name, list_id=list_id, + entity_id=entity_id, position=position, label=label, attrib=attrib, diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 99a71dd04..17827266d 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -316,6 +316,7 @@ def create_entity_list_item( self, project_name: str, list_id: str, + entity_id: str, *, position: Optional[int] = None, label: Optional[str] = None, @@ -329,6 +330,7 @@ def create_entity_list_item( Args: project_name (str): Project name where entity list lives. list_id (str): Entity list id where item will be added. + entity_id (str): Id of entity added to the list. position (Optional[int]): Position of item in entity list. label (Optional[str]): Label of item in entity list. attrib (Optional[dict[str, Any]]): Item attribute values. @@ -343,10 +345,9 @@ def create_entity_list_item( if item_id is None: item_id = create_entity_id() - data = data or {} kwargs = { "id": item_id, - "entityId": data.pop("entityId"), + "entityId": entity_id, } for key, value in ( ("position", position), From 719b8ad8ca1da91514d1e7f0741d1f63f93c1e4f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:31:32 +0200 Subject: [PATCH 401/506] add list to attribute scope literal --- ayon_api/typing.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 0bcf2d9d4..68394c046 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -103,7 +103,8 @@ class BackgroundOperationTask(TypedDict): "version", "representation", "workfile", - "user" + "user", + "list", ] AttributeType = Literal[ From fc2215f85036fab4086930be22e1a7696d09983a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 16 Apr 2026 10:52:14 +0200 Subject: [PATCH 402/506] better function name --- ayon_api/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 5f7bf07c4..7294c110b 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -256,7 +256,7 @@ def fill_own_attribs(entity: AnyEntityDict) -> None: own_attrib[key] = copy.deepcopy(value) -def _convert_list_filter_value(value: Any) -> Optional[list[Any]]: +def _convert_filter_value(value: Any) -> Optional[list[Any]]: if value is None: return None @@ -272,7 +272,7 @@ def prepare_list_filters( output: dict[str, Any], *args: tuple[str, Any], **kwargs: Any ) -> bool: for key, value in itertools.chain(args, kwargs.items()): - value = _convert_list_filter_value(value) + value = _convert_filter_value(value) if value is None: continue if not value: From 96d8ba6a873bd54745ed951de4d7763d6449bc27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:00:16 +0200 Subject: [PATCH 403/506] added more activity methods --- ayon_api/_api_helpers/activities.py | 112 +++++++++++++++++++++++++++- 1 file changed, 111 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/activities.py b/ayon_api/_api_helpers/activities.py index f7ae3d4f7..52395d66b 100644 --- a/ayon_api/_api_helpers/activities.py +++ b/ayon_api/_api_helpers/activities.py @@ -2,7 +2,7 @@ import json import typing -from typing import Optional, Iterable, Generator, Any +from typing import Optional, Iterable, Generator, Any, Literal from ayon_api.utils import ( SortOrder, @@ -255,6 +255,116 @@ def delete_activity(self, project_name: str, activity_id: str) -> None: ) response.raise_for_status() + def get_raw_activity_categories(self, project_name: str) -> dict[str, Any]: + """Get activity categories available on server (raw response). + + Args: + project_name (str): Project name to get categories for. + + Returns: + list[str]: Available activity categories. + + """ + response = self.get(f"projects/{project_name}/activityCategories") + response.raise_for_status() + return response.data + + def get_activity_categories(self, project_name: str) -> list[str]: + """Get activity categories available on server. + + Args: + project_name (str): Project name to get categories for. + + Returns: + list[str]: Available activity categories. + + """ + + data = self.get_raw_activity_categories(project_name) + return data["categories"] + + def create_activity_reaction( + self, + project_name: str, + activity_id: str, + reaction: str, + ) -> None: + """React to activity.""" + response = self.post( + f"projects/{project_name}/activities/{activity_id}/reactions", + reaction=reaction, + ) + response.raise_for_status() + + def delete_activity_reaction( + self, project_name: str, activity_id: str, reaction: str + ) -> None: + response = self.delete( + f"projects/{project_name}/activities/{activity_id}" + f"/reactions/{reaction}" + ) + response.raise_for_status() + + def suggest_entity_mention( + self, + project_name: str, + entity_id: str, + entity_type: Literal["folder", "task", "version"], + ) -> dict[str, dict[str, Any]]: + """Suggest entities for mention in activity body. + + At this moment does not change data only returns suggestions. + + Args: + project_name (str): Project name to search in. + entity_id (str): Entity id. + entity_type (str): Entity type of the entity. + + Returns: + list[dict[str, Any]]: List of suggested entities with their details. + + """ + response = self.post( + f"projects/{project_name}/suggest", + entity_id=entity_id, + entity_type=entity_type, + ) + response.raise_for_status() + return response.data + + def get_raw_entity_watchers( + self, project_name: str, entity_id: str, entity_type: str + ) -> dict[str, Any]: + """Get entity watchers (raw response).""" + response = self.get( + f"projects/{project_name}/{entity_type}/{entity_id}/watchers" + ) + response.raise_for_status() + return response.data + + def get_entity_watchers( + self, project_name: str, entity_id: str, entity_type: str + ) -> list[str]: + """List watchers of an entity.""" + data = self.get_raw_entity_watchers( + project_name, entity_id, entity_type + ) + return data["watchers"] + + def set_entity_watchers( + self, + project_name: str, + entity_id: str, + entity_type: str, + watchers: list[str], + ): + """Change watchers of an entity.""" + response = self.post( + f"projects/{project_name}/{entity_type}/{entity_id}/watchers", + watchers=watchers, + ) + response.raise_for_status() + def send_activities_batch_operations( self, project_name: str, From a8725fd2271fa8b4a612b9289ac00c5d9159ca9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:00:29 +0200 Subject: [PATCH 404/506] rename 'remove_attribute_config' to 'delete_attribute_config' --- ayon_api/_api_helpers/attributes.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/attributes.py b/ayon_api/_api_helpers/attributes.py index c9deb6356..dbd2544b9 100644 --- a/ayon_api/_api_helpers/attributes.py +++ b/ayon_api/_api_helpers/attributes.py @@ -139,7 +139,7 @@ def set_attribute_config( self.reset_attributes_schema() - def remove_attribute_config(self, attribute_name: str) -> None: + def delete_attribute_config(self, attribute_name: str) -> None: """Remove attribute from server. This can't be un-done, please use carefully. @@ -156,6 +156,17 @@ def remove_attribute_config(self, attribute_name: str) -> None: self.reset_attributes_schema() + def remove_attribute_config(self, attribute_name: str) -> None: + """Remove attribute from server. + + DEPRECATED: Use 'delete_attribute_config' instead. + + Args: + attribute_name (str): Name of attribute to remove. + + """ + return self.delete_attribute_config(attribute_name) + def get_attributes_for_type( self, entity_type: AttributeScope ) -> dict[str, AttributeSchemaDataDict]: From 1132f8dcc0fa788f54132aa637f30879bd5e9c2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:00:45 +0200 Subject: [PATCH 405/506] added server config methods --- ayon_api/server_api.py | 172 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 171 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7ad2f5ba0..7b60d68a7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -16,7 +16,7 @@ import uuid from contextlib import contextmanager import typing -from typing import Optional, Iterable, Generator, Any, Union +from typing import Optional, Iterable, Generator, Any, Union, Literal import requests @@ -1361,6 +1361,176 @@ def get(self, entrypoint: str, **kwargs): def delete(self, entrypoint: str, **kwargs): return self.raw_delete(entrypoint, params=kwargs) + def get_server_config(self): + response = self.get("config") + response.raise_for_status() + return response.data + + def set_server_config( + self, + studio_name: str | None = None, + customization: dict[str, Any] | None = None, + authentication: dict[str, Any] | None = None, + project_options: dict[str, Any] | None = None, + changelog: dict[str, Any] | None = None, + ) -> None: + body = { + key: value + for key, value in ( + ("studio_name", studio_name), + ("customization", customization), + ("authentication", authentication), + ("project_options", project_options), + ("changelog", changelog), + ) + if value is not None + } + response = self.post("config", **body) + response.raise_for_status() + + def get_server_config_overrides(self): + response = self.get("config/overrides") + response.raise_for_status() + return response.data + + def get_server_config_value(self, key: str): + response = self.get(f"config/value/{key}") + response.raise_for_status() + return response.data + + def download_server_config_file( + self, + file_type: Literal["login_background", "studio_logo"], + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + """Download server config file. + + Validate if server has config file available first. Method crashes + if the file is not available. + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + filepath (str): Target filepath. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + """ + return self.download_file( + f"api/config/files/{file_type}", + filepath, + chunk_size=chunk_size, + progress=progress, + ) + + def download_server_config_file_to_stream( + self, + file_type: Literal["login_background", "studio_logo"], + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, + ) -> TransferProgress: + """Download server config file to byte stream. + + Validate if server has config file available first. Method crashes + if the file is not available. + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + stream (StreamType): Stream where downloaded content is stored. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + """ + return self.download_file_to_stream( + f"api/config/files/{file_type}", + stream, + chunk_size=chunk_size, + progress=progress, + ) + + def upload_server_config_file( + self, + file_type: Literal["login_background", "studio_logo"], + filepath: str, + *, + content_type: str | None = None, + filename: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, + ) -> requests.Response: + """Upload server config file from byte stream. + + TODO create filename using file_type and extension from content_type + if filename is not specified + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + filepath (str): Filepath used to store the file. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + Returns: + requests.Response: Response from upload. + + """ + if not filename: + filename = os.path.basename(filepath) + return self.upload_file( + f"api/config/files/{file_type}", + filepath, + filename=filename, + content_type=content_type, + chunk_size=chunk_size, + progress=progress, + ) + + def upload_server_config_file_from_stream( + self, + file_type: Literal["login_background", "studio_logo"], + stream: StreamType, + filename: str, + *, + content_type: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, + ) -> requests.Response: + """Upload server config file from byte stream. + + TODO create filename using file_type and extension from content_type + if filename is not specified + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + stream (StreamType): Stream where downloaded content is stored. + filename (str): Filename used to store the file. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + Returns: + requests.Response: Response from upload. + + """ + return self.upload_file_from_stream( + f"api/config/files/{file_type}", + stream, + filename=filename, + content_type=content_type, + chunk_size=chunk_size, + progress=progress, + ) + def _endpoint_to_url( self, endpoint: str, From 17e8c318d2b535c9f44de2727198745c1cfa7ad3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:03:08 +0200 Subject: [PATCH 406/506] added project folders methods --- ayon_api/_api_helpers/projects.py | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 58857248e..f0a757a5a 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -498,6 +498,72 @@ def delete_project(self, project_name: str): f"Failed to delete project \"{project_name}\". {detail}" ) + def get_raw_project_folders(self) -> dict[str, Any]: + """Get project folders (raw data).""" + response = self.get("projectFolders") + response.raise_for_status() + return response.data + + def get_project_folders(self) -> list[dict[str, Any]]: + data = self.get_raw_project_folders() + return data["folders"] + + def create_project_folder( + self, + label: str, + parent_id: str | None = None, + data: dict[str, Any] | None = None, + ) -> str: + """Create project folder.""" + kwargs = {} + if parent_id is not None: + kwargs["parentId"] = parent_id + if data: + kwargs["data"] = data + + response = self.post("projectFolders", label=label, **kwargs) + response.raise_for_status() + return response.data["id"] + + def update_project_folder( + self, + folder_id: str, + label: str | None = None, + parent_id: str | None = None, + data: dict[str, Any] | None = None, + ) -> None: + body = { + key: value + for key, value in ( + ("label", label), + ("parentId", parent_id), + ("data", data), + ) + if value is not None + } + response = self.patch(f"projectFolders/{folder_id}", **body) + response.raise_for_status() + + def set_project_folders_order(self, folder_ids: list[str]) -> None: + """Set project folders order.""" + response = self.post("projectFolders/order", order=folder_ids) + response.raise_for_status() + + def assign_projects_to_project_folder( + self, folder_id: str, project_names: list[str], + ) -> None: + """Assign project folder to project.""" + response = self.post( + f"projectFolders/assign", + folderId=folder_id, + projectNames=project_names, + ) + response.raise_for_status() + + def delete_project_folder(self, folder_id: str): + """Delete project folder.""" + response = self.delete(f"projectFolders/{folder_id}") + def get_project_root_overrides( self, project_name: str ) -> dict[str, dict[str, str]]: From 9aea67450091acb5e6c0f273c4ac0dc6ab1107da Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 28 Apr 2026 16:55:45 +0200 Subject: [PATCH 407/506] added skeleton filtering to project methods --- ayon_api/_api_helpers/projects.py | 49 +++++++++++++++++++++++++------ ayon_api/graphql_queries.py | 2 ++ 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index f0a757a5a..3177b7c18 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -174,6 +174,7 @@ def get_rest_projects( self, active: Optional[bool] = True, library: Optional[bool] = None, + skeleton: bool = False, ) -> Generator[ProjectDict, None, None]: """Query available project entities. @@ -184,12 +185,13 @@ def get_rest_projects( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. + skeleton (bool): Include skeleton projects. Returns: Generator[ProjectDict, None, None]: Available projects. """ - for project_name in self.get_project_names(active, library): + for project_name in self.get_project_names(active, library, skeleton): project = self.get_rest_project(project_name) if project: yield project @@ -198,6 +200,7 @@ def get_rest_projects_list( self, active: Optional[bool] = True, library: Optional[bool] = None, + skeleton: bool = False, ) -> list[ProjectListDict]: """Receive available projects. @@ -208,6 +211,7 @@ def get_rest_projects_list( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. + skeleton (bool): Include skeleton projects. Returns: list[ProjectListDict]: List of available projects. @@ -219,10 +223,14 @@ def get_rest_projects_list( if library is not None: library = "true" if library else "false" - query = prepare_query_string({ + query_data = { "active": active, "library": library, - }) + } + if skeleton: + query_data["skeleton"] = "true" + + query = prepare_query_string(query_data) response = self.get(f"projects{query}") response.raise_for_status() data = response.data @@ -232,6 +240,7 @@ def get_project_names( self, active: Optional[bool] = True, library: Optional[bool] = None, + skeleton: bool = False, ) -> list[str]: """Receive available project names. @@ -242,6 +251,7 @@ def get_project_names( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. + skeleton (bool): Include skeleton projects. Returns: list[str]: List of available project names. @@ -249,13 +259,16 @@ def get_project_names( """ return [ project["name"] - for project in self.get_rest_projects_list(active, library) + for project in self.get_rest_projects_list( + active, library, skeleton + ) ] def get_projects( self, active: Optional[bool] = True, library: Optional[bool] = None, + skeleton: bool = False, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, ) -> Generator[ProjectDict, None, None]: @@ -280,7 +293,7 @@ def get_projects( graphql_fields, fetch_type = self._get_project_graphql_fields(fields) if fetch_type == ProjectFetchType.RESTList: - yield from self.get_rest_projects_list(active, library) + yield from self.get_rest_projects_list(active, library, skeleton) return projects_by_name = {} @@ -288,6 +301,7 @@ def get_projects( projects = list(self._get_graphql_projects( active, library, + skeleton=skeleton, fields=graphql_fields, own_attributes=own_attributes, )) @@ -296,7 +310,9 @@ def get_projects( return projects_by_name = {p["name"]: p for p in projects} - for project in self.get_rest_projects(active=active, library=library): + for project in self.get_rest_projects( + active=active, library=library, skeleton=skeleton + ): if own_attributes: fill_own_attribs(project) @@ -356,6 +372,8 @@ def create_project( project_code: str, library_project: bool = False, preset_name: Optional[str] = None, + data: dict[str, Any] | None = None, + skeleton: bool = False, ) -> ProjectDict: """Create project using AYON settings. @@ -375,6 +393,8 @@ def create_project( library_project (Optional[bool]): Project is library project. preset_name (Optional[str]): Name of anatomy preset. Default is used if not passed. + data (dict[str, Any]): Project data. + skeleton (bool): Project is skeleton project. Raises: ValueError: When project name already exists. @@ -395,12 +415,19 @@ def create_project( preset = self.get_project_anatomy_preset(preset_name) + if data is None: + data = {} + + if skeleton: + data["skeleton"] = True + result = self.post( "projects", name=project_name, code=project_code, anatomy=preset, - library=library_project + library=library_project, + data=data, ) if result.status != 201: @@ -858,8 +885,9 @@ def _fill_project_entity_data(self, project: dict[str, Any]) -> None: def _get_graphql_projects( self, - active: Optional[bool], - library: Optional[bool], + active: bool | None, + library: bool | None, + skeleton: bool, fields: set[str], own_attributes: bool, project_name: Optional[str] = None @@ -876,6 +904,9 @@ def _get_graphql_projects( if project_name is not None: query.set_variable_value("projectName", project_name) + if skeleton: + query.set_variable_value("skeleton", True) + attributes = {} if "allAttrib" in fields: attributes = self.get_attributes_for_type("project") diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 100c0aa9d..d376d0d43 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -91,8 +91,10 @@ def project_graphql_query(fields): def projects_graphql_query(fields): query = GraphQlQuery("ProjectsQuery") project_name_var = query.add_variable("projectName", "String!") + skeleton_var = query.add_variable("skeleton", "Boolean!") projects_field = query.add_field_with_edges("projects") projects_field.set_filter("name", project_name_var) + projects_field.set_filter("includeSkeleton", skeleton_var) nested_fields = fields_to_dict(fields) From 31129cb462811d7f0803d1ec579312ecb425da73 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:23:00 +0200 Subject: [PATCH 408/506] rename skeleton argument to 'include_skeleton' --- ayon_api/_api_helpers/projects.py | 43 ++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 3177b7c18..c5e6eb715 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -174,7 +174,7 @@ def get_rest_projects( self, active: Optional[bool] = True, library: Optional[bool] = None, - skeleton: bool = False, + include_skeleton: bool = False, ) -> Generator[ProjectDict, None, None]: """Query available project entities. @@ -185,13 +185,17 @@ def get_rest_projects( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. - skeleton (bool): Include skeleton projects. + include_skeleton (bool): Include skeleton projects. Returns: Generator[ProjectDict, None, None]: Available projects. """ - for project_name in self.get_project_names(active, library, skeleton): + for project_name in self.get_project_names( + active=active, + library=library, + include_skeleton=include_skeleton, + ): project = self.get_rest_project(project_name) if project: yield project @@ -200,7 +204,7 @@ def get_rest_projects_list( self, active: Optional[bool] = True, library: Optional[bool] = None, - skeleton: bool = False, + include_skeleton: bool = False, ) -> list[ProjectListDict]: """Receive available projects. @@ -211,7 +215,7 @@ def get_rest_projects_list( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. - skeleton (bool): Include skeleton projects. + include_skeleton (bool): Include skeleton projects. Returns: list[ProjectListDict]: List of available projects. @@ -227,7 +231,7 @@ def get_rest_projects_list( "active": active, "library": library, } - if skeleton: + if include_skeleton: query_data["skeleton"] = "true" query = prepare_query_string(query_data) @@ -240,7 +244,7 @@ def get_project_names( self, active: Optional[bool] = True, library: Optional[bool] = None, - skeleton: bool = False, + include_skeleton: bool = False, ) -> list[str]: """Receive available project names. @@ -251,7 +255,7 @@ def get_project_names( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. - skeleton (bool): Include skeleton projects. + include_skeleton (bool): Include skeleton projects. Returns: list[str]: List of available project names. @@ -260,7 +264,9 @@ def get_project_names( return [ project["name"] for project in self.get_rest_projects_list( - active, library, skeleton + active=active, + library=library, + include_skeleton=include_skeleton, ) ] @@ -268,7 +274,7 @@ def get_projects( self, active: Optional[bool] = True, library: Optional[bool] = None, - skeleton: bool = False, + include_skeleton: bool = False, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, ) -> Generator[ProjectDict, None, None]: @@ -279,6 +285,7 @@ def get_projects( Filter is disabled when 'None' is passed. library (Optional[bool]): Filter library projects. Filter is disabled when 'None' is passed. + include_skeleton (bool): Include skeleton projects. fields (Optional[Iterable[str]]): fields to be queried for project. own_attributes (Optional[bool]): Attribute values that are @@ -293,7 +300,11 @@ def get_projects( graphql_fields, fetch_type = self._get_project_graphql_fields(fields) if fetch_type == ProjectFetchType.RESTList: - yield from self.get_rest_projects_list(active, library, skeleton) + yield from self.get_rest_projects_list( + active=active, + library=library, + include_skeleton=include_skeleton, + ) return projects_by_name = {} @@ -301,7 +312,7 @@ def get_projects( projects = list(self._get_graphql_projects( active, library, - skeleton=skeleton, + include_skeleton=include_skeleton, fields=graphql_fields, own_attributes=own_attributes, )) @@ -311,7 +322,9 @@ def get_projects( projects_by_name = {p["name"]: p for p in projects} for project in self.get_rest_projects( - active=active, library=library, skeleton=skeleton + active=active, + library=library, + include_skeleton=include_skeleton, ): if own_attributes: fill_own_attribs(project) @@ -887,7 +900,7 @@ def _get_graphql_projects( self, active: bool | None, library: bool | None, - skeleton: bool, + include_skeleton: bool, fields: set[str], own_attributes: bool, project_name: Optional[str] = None @@ -904,7 +917,7 @@ def _get_graphql_projects( if project_name is not None: query.set_variable_value("projectName", project_name) - if skeleton: + if include_skeleton: query.set_variable_value("skeleton", True) attributes = {} From fecd3254f8c507a5a51e2933b9717a2165828c83 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:23:39 +0200 Subject: [PATCH 409/506] update public api --- ayon_api/__init__.py | 48 +++++ ayon_api/_api.py | 422 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 469 insertions(+), 1 deletion(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index c964c24ef..a125e8993 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -69,6 +69,14 @@ patch, get, delete, + get_server_config, + set_server_config, + get_server_config_overrides, + get_server_config_value, + download_server_config_file, + download_server_config_file_to_stream, + upload_server_config_file, + upload_server_config_file_from_stream, download_file_to_stream, download_file, upload_project_file, @@ -116,6 +124,14 @@ create_activity, update_activity, delete_activity, + get_raw_activity_categories, + get_activity_categories, + create_activity_reaction, + delete_activity_reaction, + suggest_entity_mention, + get_raw_entity_watchers, + get_entity_watchers, + set_entity_watchers, send_activities_batch_operations, get_bundles, create_bundle, @@ -151,6 +167,7 @@ reset_attributes_cache, set_attributes_cache_timeout, set_attribute_config, + delete_attribute_config, remove_attribute_config, get_attributes_for_type, get_attributes_fields_for_type, @@ -168,6 +185,13 @@ create_project, update_project, delete_project, + get_raw_project_folders, + get_project_folders, + create_project_folder, + update_project_folder, + set_project_folders_order, + assign_projects_to_project_folder, + delete_project_folder, get_project_root_overrides, get_project_roots_by_site, get_project_root_overrides_by_site_id, @@ -356,6 +380,14 @@ "patch", "get", "delete", + "get_server_config", + "set_server_config", + "get_server_config_overrides", + "get_server_config_value", + "download_server_config_file", + "download_server_config_file_to_stream", + "upload_server_config_file", + "upload_server_config_file_from_stream", "download_file_to_stream", "download_file", "upload_project_file", @@ -403,6 +435,14 @@ "create_activity", "update_activity", "delete_activity", + "get_raw_activity_categories", + "get_activity_categories", + "create_activity_reaction", + "delete_activity_reaction", + "suggest_entity_mention", + "get_raw_entity_watchers", + "get_entity_watchers", + "set_entity_watchers", "send_activities_batch_operations", "get_bundles", "create_bundle", @@ -438,6 +478,7 @@ "reset_attributes_cache", "set_attributes_cache_timeout", "set_attribute_config", + "delete_attribute_config", "remove_attribute_config", "get_attributes_for_type", "get_attributes_fields_for_type", @@ -455,6 +496,13 @@ "create_project", "update_project", "delete_project", + "get_raw_project_folders", + "get_project_folders", + "create_project_folder", + "update_project_folder", + "set_project_folders_order", + "assign_projects_to_project_folder", + "delete_project_folder", "get_project_root_overrides", "get_project_roots_by_site", "get_project_root_overrides_by_site_id", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 98e794203..1dcf2b74a 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -926,6 +926,177 @@ def delete( ) +def get_server_config(): + con = get_server_api_connection() + return con.get_server_config() + + +def set_server_config( + studio_name: str | None = None, + customization: dict[str, Any] | None = None, + authentication: dict[str, Any] | None = None, + project_options: dict[str, Any] | None = None, + changelog: dict[str, Any] | None = None, +) -> None: + con = get_server_api_connection() + return con.set_server_config( + studio_name=studio_name, + customization=customization, + authentication=authentication, + project_options=project_options, + changelog=changelog, + ) + + +def get_server_config_overrides(): + con = get_server_api_connection() + return con.get_server_config_overrides() + + +def get_server_config_value( + key: str, +): + con = get_server_api_connection() + return con.get_server_config_value( + key=key, + ) + + +def download_server_config_file( + file_type: Literal[login_background, studio_logo], + filepath: str, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> TransferProgress: + """Download server config file. + + Validate if server has config file available first. Method crashes + if the file is not available. + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + filepath (str): Target filepath. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + """ + con = get_server_api_connection() + return con.download_server_config_file( + file_type=file_type, + filepath=filepath, + chunk_size=chunk_size, + progress=progress, + ) + + +def download_server_config_file_to_stream( + file_type: Literal[login_background, studio_logo], + stream: StreamType, + *, + chunk_size: Optional[int] = None, + progress: Optional[TransferProgress] = None, +) -> TransferProgress: + """Download server config file to byte stream. + + Validate if server has config file available first. Method crashes + if the file is not available. + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + stream (StreamType): Stream where downloaded content is stored. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + """ + con = get_server_api_connection() + return con.download_server_config_file_to_stream( + file_type=file_type, + stream=stream, + chunk_size=chunk_size, + progress=progress, + ) + + +def upload_server_config_file( + file_type: Literal[login_background, studio_logo], + filepath: str, + *, + content_type: str | None = None, + filename: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, +) -> requests.Response: + """Upload server config file from byte stream. + + TODO create filename using file_type and extension from content_type + if filename is not specified + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + filepath (str): Filepath used to store the file. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + Returns: + requests.Response: Response from upload. + + """ + con = get_server_api_connection() + return con.upload_server_config_file( + file_type=file_type, + filepath=filepath, + content_type=content_type, + filename=filename, + chunk_size=chunk_size, + progress=progress, + ) + + +def upload_server_config_file_from_stream( + file_type: Literal[login_background, studio_logo], + stream: StreamType, + filename: str, + *, + content_type: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, +) -> requests.Response: + """Upload server config file from byte stream. + + TODO create filename using file_type and extension from content_type + if filename is not specified + + Args: + file_type (Literal["login_background", "studio_logo"]): File to + download. + stream (StreamType): Stream where downloaded content is stored. + filename (str): Filename used to store the file. + chunk_size (int | None): Size of chunks used for download. + progress (TransferProgress | None): Object to track download + progress. + + Returns: + requests.Response: Response from upload. + + """ + con = get_server_api_connection() + return con.upload_server_config_file_from_stream( + file_type=file_type, + stream=stream, + filename=filename, + content_type=content_type, + chunk_size=chunk_size, + progress=progress, + ) + + def download_file_to_stream( endpoint: str, stream: StreamType, @@ -2370,6 +2541,143 @@ def delete_activity( ) +def get_raw_activity_categories( + project_name: str, +) -> dict[str, Any]: + """Get activity categories available on server (raw response). + + Args: + project_name (str): Project name to get categories for. + + Returns: + list[str]: Available activity categories. + + """ + con = get_server_api_connection() + return con.get_raw_activity_categories( + project_name=project_name, + ) + + +def get_activity_categories( + project_name: str, +) -> list[str]: + """Get activity categories available on server. + + Args: + project_name (str): Project name to get categories for. + + Returns: + list[str]: Available activity categories. + + """ + con = get_server_api_connection() + return con.get_activity_categories( + project_name=project_name, + ) + + +def create_activity_reaction( + project_name: str, + activity_id: str, + reaction: str, +) -> None: + """React to activity. + """ + con = get_server_api_connection() + return con.create_activity_reaction( + project_name=project_name, + activity_id=activity_id, + reaction=reaction, + ) + + +def delete_activity_reaction( + project_name: str, + activity_id: str, + reaction: str, +) -> None: + con = get_server_api_connection() + return con.delete_activity_reaction( + project_name=project_name, + activity_id=activity_id, + reaction=reaction, + ) + + +def suggest_entity_mention( + project_name: str, + entity_id: str, + entity_type: Literal[folder, task, version], +) -> dict[str, dict[str, Any]]: + """Suggest entities for mention in activity body. + + At this moment does not change data only returns suggestions. + + Args: + project_name (str): Project name to search in. + entity_id (str): Entity id. + entity_type (str): Entity type of the entity. + + Returns: + list[dict[str, Any]]: List of suggested entities with their details. + + """ + con = get_server_api_connection() + return con.suggest_entity_mention( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + ) + + +def get_raw_entity_watchers( + project_name: str, + entity_id: str, + entity_type: str, +) -> dict[str, Any]: + """Get entity watchers (raw response). + """ + con = get_server_api_connection() + return con.get_raw_entity_watchers( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + ) + + +def get_entity_watchers( + project_name: str, + entity_id: str, + entity_type: str, +) -> list[str]: + """List watchers of an entity. + """ + con = get_server_api_connection() + return con.get_entity_watchers( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + ) + + +def set_entity_watchers( + project_name: str, + entity_id: str, + entity_type: str, + watchers: list[str], +): + """Change watchers of an entity. + """ + con = get_server_api_connection() + return con.set_entity_watchers( + project_name=project_name, + entity_id=entity_id, + entity_type=entity_type, + watchers=watchers, + ) + + def send_activities_batch_operations( project_name: str, operations: list[dict[str, Any]], @@ -3622,13 +3930,30 @@ def set_attribute_config( ) -def remove_attribute_config( +def delete_attribute_config( attribute_name: str, ) -> None: """Remove attribute from server. This can't be un-done, please use carefully. + Args: + attribute_name (str): Name of attribute to remove. + + """ + con = get_server_api_connection() + return con.delete_attribute_config( + attribute_name=attribute_name, + ) + + +def remove_attribute_config( + attribute_name: str, +) -> None: + """Remove attribute from server. + + DEPRECATED: Use 'delete_attribute_config' instead. + Args: attribute_name (str): Name of attribute to remove. @@ -3803,6 +4128,7 @@ def get_rest_project( def get_rest_projects( active: Optional[bool] = True, library: Optional[bool] = None, + include_skeleton: bool = False, ) -> Generator[ProjectDict, None, None]: """Query available project entities. @@ -3813,6 +4139,7 @@ def get_rest_projects( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. + include_skeleton (bool): Include skeleton projects. Returns: Generator[ProjectDict, None, None]: Available projects. @@ -3822,12 +4149,14 @@ def get_rest_projects( return con.get_rest_projects( active=active, library=library, + include_skeleton=include_skeleton, ) def get_rest_projects_list( active: Optional[bool] = True, library: Optional[bool] = None, + include_skeleton: bool = False, ) -> list[ProjectListDict]: """Receive available projects. @@ -3838,6 +4167,7 @@ def get_rest_projects_list( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. + include_skeleton (bool): Include skeleton projects. Returns: list[ProjectListDict]: List of available projects. @@ -3847,12 +4177,14 @@ def get_rest_projects_list( return con.get_rest_projects_list( active=active, library=library, + include_skeleton=include_skeleton, ) def get_project_names( active: Optional[bool] = True, library: Optional[bool] = None, + include_skeleton: bool = False, ) -> list[str]: """Receive available project names. @@ -3863,6 +4195,7 @@ def get_project_names( are returned if 'None' is passed. library (Optional[bool]): Filter standard/library projects. Both are returned if 'None' is passed. + include_skeleton (bool): Include skeleton projects. Returns: list[str]: List of available project names. @@ -3872,12 +4205,14 @@ def get_project_names( return con.get_project_names( active=active, library=library, + include_skeleton=include_skeleton, ) def get_projects( active: Optional[bool] = True, library: Optional[bool] = None, + include_skeleton: bool = False, fields: Optional[Iterable[str]] = None, own_attributes: bool = False, ) -> Generator[ProjectDict, None, None]: @@ -3888,6 +4223,7 @@ def get_projects( Filter is disabled when 'None' is passed. library (Optional[bool]): Filter library projects. Filter is disabled when 'None' is passed. + include_skeleton (bool): Include skeleton projects. fields (Optional[Iterable[str]]): fields to be queried for project. own_attributes (Optional[bool]): Attribute values that are @@ -3901,6 +4237,7 @@ def get_projects( return con.get_projects( active=active, library=library, + include_skeleton=include_skeleton, fields=fields, own_attributes=own_attributes, ) @@ -3938,6 +4275,8 @@ def create_project( project_code: str, library_project: bool = False, preset_name: Optional[str] = None, + data: dict[str, Any] | None = None, + skeleton: bool = False, ) -> ProjectDict: """Create project using AYON settings. @@ -3957,6 +4296,8 @@ def create_project( library_project (Optional[bool]): Project is library project. preset_name (Optional[str]): Name of anatomy preset. Default is used if not passed. + data (dict[str, Any]): Project data. + skeleton (bool): Project is skeleton project. Raises: ValueError: When project name already exists. @@ -3971,6 +4312,8 @@ def create_project( project_code=project_code, library_project=library_project, preset_name=preset_name, + data=data, + skeleton=skeleton, ) @@ -4049,6 +4392,83 @@ def delete_project( ) +def get_raw_project_folders() -> dict[str, Any]: + """Get project folders (raw data). + """ + con = get_server_api_connection() + return con.get_raw_project_folders() + + +def get_project_folders() -> list[dict[str, Any]]: + con = get_server_api_connection() + return con.get_project_folders() + + +def create_project_folder( + label: str, + parent_id: str | None = None, + data: dict[str, Any] | None = None, +) -> str: + """Create project folder. + """ + con = get_server_api_connection() + return con.create_project_folder( + label=label, + parent_id=parent_id, + data=data, + ) + + +def update_project_folder( + folder_id: str, + label: str | None = None, + parent_id: str | None = None, + data: dict[str, Any] | None = None, +) -> None: + con = get_server_api_connection() + return con.update_project_folder( + folder_id=folder_id, + label=label, + parent_id=parent_id, + data=data, + ) + + +def set_project_folders_order( + folder_ids: list[str], +) -> None: + """Set project folders order. + """ + con = get_server_api_connection() + return con.set_project_folders_order( + folder_ids=folder_ids, + ) + + +def assign_projects_to_project_folder( + folder_id: str, + project_names: list[str], +) -> None: + """Assign project folder to project. + """ + con = get_server_api_connection() + return con.assign_projects_to_project_folder( + folder_id=folder_id, + project_names=project_names, + ) + + +def delete_project_folder( + folder_id: str, +): + """Delete project folder. + """ + con = get_server_api_connection() + return con.delete_project_folder( + folder_id=folder_id, + ) + + def get_project_root_overrides( project_name: str, ) -> dict[str, dict[str, str]]: From efeb380aa6355ec22233dd22bd7c875efaf0bcf5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:57:44 +0200 Subject: [PATCH 410/506] remove unnecessafy f-string --- ayon_api/_api_helpers/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index c5e6eb715..88e55068f 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -594,7 +594,7 @@ def assign_projects_to_project_folder( ) -> None: """Assign project folder to project.""" response = self.post( - f"projectFolders/assign", + "projectFolders/assign", folderId=folder_id, projectNames=project_names, ) From e95d519b0833a2512c09e87ccfd71c3810c7e582 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:01 +0200 Subject: [PATCH 411/506] raise for status --- ayon_api/_api_helpers/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 88e55068f..0cadfeef8 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -603,6 +603,7 @@ def assign_projects_to_project_folder( def delete_project_folder(self, folder_id: str): """Delete project folder.""" response = self.delete(f"projectFolders/{folder_id}") + response.raise_for_status() def get_project_root_overrides( self, project_name: str From e793217bf34689b7a69a334a7aeffae777a5b2d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:10 +0200 Subject: [PATCH 412/506] shorten line --- ayon_api/_api_helpers/activities.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/activities.py b/ayon_api/_api_helpers/activities.py index 52395d66b..2428e3b54 100644 --- a/ayon_api/_api_helpers/activities.py +++ b/ayon_api/_api_helpers/activities.py @@ -321,7 +321,8 @@ def suggest_entity_mention( entity_type (str): Entity type of the entity. Returns: - list[dict[str, Any]]: List of suggested entities with their details. + list[dict[str, Any]]: List of suggested entities with + their details. """ response = self.post( From 6ad135abf911590a83cd2e6ae97435f9c242954d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:18 +0200 Subject: [PATCH 413/506] fix literals in automated api --- automated_api.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/automated_api.py b/automated_api.py index 9565717bd..766cd4bc3 100644 --- a/automated_api.py +++ b/automated_api.py @@ -154,6 +154,7 @@ def _get_typehint(annotation, api_globals): str(annotation) .replace("NoneType", "None") ) + full_path_regex = re.compile( r"(?P(?P[a-zA-Z0-9_\.]+))" ) @@ -181,6 +182,17 @@ def _get_typehint(annotation, api_globals): name = name.split(".")[-1] typehint = typehint.replace(groups["full"], name) + if "Literal" in typehint: + for match in re.finditer( + r"(?PLiteral\[(?P[^\]]*)\])", typehint + ): + full_content = match.group("fullcontent") + content = match.group("content") + items = [f'"{i.strip()}"' for i in content.split(",")] + new_content = ", ".join(items) + new_full_content = full_content.replace(content, new_content) + typehint = typehint.replace(full_content, new_full_content) + try: # Test if typehint is valid for known '_api' content exec(f"_: {typehint} = None", api_globals) From d41c7c6e95436bbbfb0a7949c1f531ca0718ec01 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:58:29 +0200 Subject: [PATCH 414/506] update public api --- ayon_api/_api.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 1dcf2b74a..5c843360f 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -41,7 +41,7 @@ ) if typing.TYPE_CHECKING: - from typing import Union + from typing import Union, Literal from .typing import ( ServerVersion, ActivityType, @@ -963,7 +963,7 @@ def get_server_config_value( def download_server_config_file( - file_type: Literal[login_background, studio_logo], + file_type: Literal["login_background", "studio_logo"], filepath: str, *, chunk_size: Optional[int] = None, @@ -993,7 +993,7 @@ def download_server_config_file( def download_server_config_file_to_stream( - file_type: Literal[login_background, studio_logo], + file_type: Literal["login_background", "studio_logo"], stream: StreamType, *, chunk_size: Optional[int] = None, @@ -1023,7 +1023,7 @@ def download_server_config_file_to_stream( def upload_server_config_file( - file_type: Literal[login_background, studio_logo], + file_type: Literal["login_background", "studio_logo"], filepath: str, *, content_type: str | None = None, @@ -1060,7 +1060,7 @@ def upload_server_config_file( def upload_server_config_file_from_stream( - file_type: Literal[login_background, studio_logo], + file_type: Literal["login_background", "studio_logo"], stream: StreamType, filename: str, *, @@ -2608,7 +2608,7 @@ def delete_activity_reaction( def suggest_entity_mention( project_name: str, entity_id: str, - entity_type: Literal[folder, task, version], + entity_type: Literal["folder", "task", "version"], ) -> dict[str, dict[str, Any]]: """Suggest entities for mention in activity body. @@ -2620,7 +2620,8 @@ def suggest_entity_mention( entity_type (str): Entity type of the entity. Returns: - list[dict[str, Any]]: List of suggested entities with their details. + list[dict[str, Any]]: List of suggested entities with + their details. """ con = get_server_api_connection() From a8070e592e362713cd13db656745d9d60eacefb2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Apr 2026 10:04:50 +0200 Subject: [PATCH 415/506] use 3.11 for global api check --- .github/workflows/check_global_api.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check_global_api.yml b/.github/workflows/check_global_api.yml index cdbb600cf..3119ae88d 100644 --- a/.github/workflows/check_global_api.yml +++ b/.github/workflows/check_global_api.yml @@ -13,7 +13,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.9' + python-version: '3.11' - name: Install dependencies run: | From 1b3333eb7f0f71c18c323f9be16d15494e3f0a8c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:25:15 +0200 Subject: [PATCH 416/506] add schema earlier --- ayon_api/utils.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 7294c110b..2cf8a5794 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -759,6 +759,11 @@ def validate_url( # Not sure if this is good idea? modified_url = stripperd_url.rstrip("/") + + # Make sure url has http schema + if not modified_url.lower().startswith("http"): + modified_url = f"http://{modified_url}" + parsed_url = _try_parse_url(modified_url) universal_hints = [ "does the url work in browser?" From 6c9dd8357ac7c3040c17e1fce6fda5354bad2d77 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:25:39 +0200 Subject: [PATCH 417/506] don't use full path just netloc on first attempt --- ayon_api/utils.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 2cf8a5794..cebde7d5e 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -777,11 +777,10 @@ def validate_url( hints=universal_hints ) - # Try add 'https://' scheme if is missing - # - this will trigger UrlError if both will crash - if not parsed_url.scheme: + if parsed_url.path: + tmp_url = f"{parsed_url.scheme}://{parsed_url.netloc}" new_url = _try_connect_to_server( - "http://" + modified_url, + tmp_url, timeout=timeout, verify=verify, cert=cert, From 94e03870216c009980163c94bcadc6549dfa60e8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:25:54 +0200 Subject: [PATCH 418/506] modified hint --- ayon_api/utils.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index cebde7d5e..892f73e8a 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -798,14 +798,9 @@ def validate_url( return new_url hints = [] - if "/" in parsed_url.path or not parsed_url.scheme: - new_path = parsed_url.path.split("/")[0] - if not parsed_url.scheme: - new_path = "https://" + new_path - - hints.append( - "did you mean \"{}\"?".format(parsed_url.scheme + new_path) - ) + if parsed_url.path: + new_path = f"{parsed_url.scheme}{parsed_url.netloc}" + hints.append(f"did you mean \"{new_path}\"?") raise UrlError( "Couldn't connect to server on \"{}\"".format(url), From 60ff60acd193ce468b21a1433bd0f258fc489940 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Apr 2026 13:40:57 +0200 Subject: [PATCH 419/506] use api info for server validation --- ayon_api/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 892f73e8a..97c977a8d 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -570,11 +570,12 @@ def _try_connect_to_server( # TODO add validation if the url lead to AYON server # - this won't validate if the url lead to 'google.com' response = requests.get( - url, + f"{url}/api/info", timeout=timeout, verify=verify, cert=cert, ) + _ = response.json() if response.history: return response.history[-1].headers["location"].rstrip("/") return url @@ -770,8 +771,9 @@ def validate_url( ] if parsed_url is None: raise UrlError( - "Invalid url format. Url cannot be parsed as url \"{}\".".format( - modified_url + ( + "Invalid url format. Url cannot be parsed" + f" as url \"{modified_url}\"." ), title="Invalid url format", hints=universal_hints From 587e818a05e0d25fd9c2f0bf2c4b7ebe59b7b68e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:20:06 +0200 Subject: [PATCH 420/506] added typehints --- ayon_api/utils.py | 157 +++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 77 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 7294c110b..e85f72270 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -12,9 +12,9 @@ import traceback import collections import itertools -from urllib.parse import urlparse, urlencode +from urllib.parse import urlparse, urlencode, ParseResult import typing -from typing import Optional, Any, Iterable, Union +from typing import Any, Iterable from enum import IntEnum import requests @@ -148,7 +148,7 @@ def content(self): return self._response.content @property - def content_type(self) -> Optional[str]: + def content_type(self) -> str | None: return self.headers.get("Content-Type") @property @@ -256,7 +256,7 @@ def fill_own_attribs(entity: AnyEntityDict) -> None: own_attrib[key] = copy.deepcopy(value) -def _convert_filter_value(value: Any) -> Optional[list[Any]]: +def _convert_filter_value(value: Any) -> list[Any] | None: if value is None: return None @@ -318,11 +318,11 @@ def get_machine_name() -> str: return unidecode.unidecode(platform.node()) -def get_default_site_id() -> Optional[str]: +def get_default_site_id() -> str | None: """Site id used for server connection. Returns: - Optional[str]: Site id from environment variable or None. + str | None: Site id from environment variable or None. """ return os.environ.get(SITE_ID_ENV_KEY) @@ -333,25 +333,25 @@ class ThumbnailContent: Args: project_name (str): Project name. - thumbnail_id (Optional[str]): Thumbnail id. - content (Optional[bytes]): Thumbnail content. - content_type (Optional[str]): Content type e.g. 'image/png'. + thumbnail_id (str | None): Thumbnail id. + content (bytes | None): Thumbnail content. + content_type (str | None): Content type e.g. 'image/png'. """ def __init__( self, project_name: str, - thumbnail_id: Optional[str], - content: Optional[bytes], - content_type: Optional[str], + thumbnail_id: str | None, + content: bytes | None, + content_type: str | None, ): self.project_name: str = project_name - self.thumbnail_id: Optional[str] = thumbnail_id - self.content_type: Optional[str] = content_type + self.thumbnail_id: str | None = thumbnail_id + self.content_type: str | None = content_type self.content: bytes = content or b"" @property - def id(self) -> str: + def id(self) -> str | None: """Wrapper for thumbnail id.""" return self.thumbnail_id @@ -394,14 +394,14 @@ def prepare_query_string( if not key_values: return "" - return "?{}".format(urlencode(key_values)) + return f"?{urlencode(key_values)}" def create_entity_id() -> str: return uuid.uuid1().hex -def convert_entity_id(entity_id) -> Optional[str]: +def convert_entity_id(entity_id) -> str | None: if not entity_id: return None @@ -416,7 +416,7 @@ def convert_entity_id(entity_id) -> Optional[str]: return None -def convert_or_create_entity_id(entity_id: Optional[str] = None) -> str: +def convert_or_create_entity_id(entity_id: str | None = None) -> str: output = convert_entity_id(entity_id) if output is None: output = create_entity_id() @@ -428,7 +428,7 @@ def entity_data_json_default(value: Any) -> Any: return int(value.timestamp()) raise TypeError( - "Object of type {} is not JSON serializable".format(str(type(value))) + f"Object of type {type(value)} is not JSON serializable" ) @@ -440,7 +440,7 @@ def slugify_string( min_length: int = 1, lower: bool = False, make_set: bool = False, -) -> Union[str, set[str]]: +) -> str | set[str]: """Slugify a text string. This function removes transliterates input string to ASCII, removes @@ -460,8 +460,7 @@ def slugify_string( min_length (int): Minimal length of an element (word). Returns: - Union[str, set[str]]: Based on 'make_set' value returns slugified - string. + str | set[str]: Based on 'make_set' value returns slugified string. """ tmp_string = unidecode.unidecode(input_string) @@ -486,14 +485,14 @@ def slugify_string( def failed_json_default(value: Any) -> str: - return "< Failed value {} > {}".format(type(value), str(value)) + return f"< Failed value {type(value)} > {value}" def prepare_attribute_changes( old_entity: AnyEntityDict, new_entity: AnyEntityDict, replace: int = False, -): +) -> dict[str, Any]: attrib_changes = {} new_attrib = new_entity.get("attrib") old_attrib = old_entity.get("attrib") @@ -544,7 +543,7 @@ def prepare_entity_changes( return changes -def _try_parse_url(url: str) -> Optional[str]: +def _try_parse_url(url: str) -> ParseResult | None: try: return urlparse(url) except BaseException: @@ -553,10 +552,10 @@ def _try_parse_url(url: str) -> Optional[str]: def _try_connect_to_server( url: str, - timeout: Optional[float], - verify: Optional[Union[str, bool]], - cert: Optional[str], -) -> Optional[str]: + timeout: float | None, + verify: str | bool | None, + cert: str | None, +) -> str | None: if timeout is None: timeout = get_default_timeout() @@ -590,19 +589,19 @@ def login_to_server( url: str, username: str, password: str, - timeout: Optional[float] = None, -) -> Optional[str]: + timeout: float | None = None, +) -> str | None: """Use login to the server to receive token. Args: url (str): Server url. username (str): User's username. password (str): User's password. - timeout (Optional[float]): Timeout for request. Value from + timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. Returns: - Optional[str]: User's token if login was successfull. + str | None: User's token if login was successfull. Otherwise 'None'. """ @@ -610,7 +609,7 @@ def login_to_server( timeout = get_default_timeout() headers = {"Content-Type": "application/json"} response = requests.post( - "{}/api/auth/login".format(url), + f"{url}/api/auth/login", headers=headers, json={ "name": username, @@ -627,13 +626,17 @@ def login_to_server( return token -def logout_from_server(url: str, token: str, timeout: Optional[float] = None): +def logout_from_server( + url: str, + token: str, + timeout: float | None = None, +) -> None: """Logout from server and throw token away. Args: url (str): Url from which should be logged out. token (str): Token which should be used to log out. - timeout (Optional[float]): Timeout for request. Value from + timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. """ @@ -641,10 +644,10 @@ def logout_from_server(url: str, token: str, timeout: Optional[float] = None): timeout = get_default_timeout() headers = { "Content-Type": "application/json", - "Authorization": "Bearer {}".format(token) + "Authorization": f"Bearer {token}", } requests.post( - url + "/api/auth/logout", + f"{url}/api/auth/logout", headers=headers, timeout=timeout, ) @@ -653,18 +656,18 @@ def logout_from_server(url: str, token: str, timeout: Optional[float] = None): def get_user_by_token( url: str, token: str, - timeout: Optional[float] = None, -) -> Optional[dict[str, Any]]: + timeout: float | None = None, +) -> dict[str, Any] | None: """Get user information by url and token. Args: url (str): Server url. token (str): User's token. - timeout (Optional[float]): Timeout for request. Value from + timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. Returns: - Optional[dict[str, Any]]: User information if url and token are valid. + dict[str, Any] | None: User information if url and token are valid. """ if timeout is None: @@ -674,13 +677,13 @@ def get_user_by_token( "Content-Type": "application/json", } for header_value in ( - {"Authorization": "Bearer {}".format(token)}, + {"Authorization": f"Bearer {token}"}, {"X-Api-Key": token}, ): headers = base_headers.copy() headers.update(header_value) response = requests.get( - "{}/api/users/me".format(url), + f"{url}/api/users/me", headers=headers, timeout=timeout, ) @@ -692,7 +695,7 @@ def get_user_by_token( def is_token_valid( url: str, token: str, - timeout: Optional[float] = None, + timeout: float | None = None, ) -> bool: """Check if token is valid. @@ -701,7 +704,7 @@ def is_token_valid( Args: url (str): Server url. token (str): User's token. - timeout (Optional[float]): Timeout for request. Value from + timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. Returns: @@ -715,9 +718,9 @@ def is_token_valid( def validate_url( url: str, - timeout: Optional[int] = None, - verify: Optional[Union[str, bool]] = None, - cert: Optional[str] = None, + timeout: int | None = None, + verify: str | bool | None = None, + cert: str | None = None, ) -> str: """Validate url if is valid and server is available. @@ -740,7 +743,7 @@ def validate_url( Args: url (str): Server url. - timeout (Optional[int]): Timeout in seconds for connection to server. + timeout (int | None): Timeout in seconds for connection to server. Returns: Url which was used to connect to server. @@ -818,25 +821,25 @@ def __init__(self): self._started: bool = False self._transfer_done: bool = False self._transferred: int = 0 - self._content_size: Optional[int] = None + self._content_size: int | None = None self._failed: bool = False - self._fail_reason: Optional[str] = None + self._fail_reason: str | None = None self._source_url: str = "N/A" self._destination_url: str = "N/A" - def get_content_size(self): + def get_content_size(self) -> int | None: """Content size in bytes. Returns: - Union[int, None]: Content size in bytes or None + int | None: Content size in bytes or None if is unknown. """ return self._content_size - def set_content_size(self, content_size: int): + def set_content_size(self, content_size: int) -> None: """Set content size in bytes. Args: @@ -859,7 +862,7 @@ def get_started(self) -> bool: """ return self._started - def set_started(self): + def set_started(self) -> None: """Mark that transfer started. Raises: @@ -890,7 +893,7 @@ def get_transfer_done(self) -> bool: """ return self._transfer_done - def set_transfer_done(self): + def set_transfer_done(self) -> None: """Mark progress as transfer finished. Raises: @@ -913,17 +916,17 @@ def get_failed(self) -> bool: """ return self._failed - def get_fail_reason(self) -> Optional[str]: + def get_fail_reason(self) -> str | None: """Get reason why transfer failed. Returns: - Optional[str]: Reason why transfer + str | None: Reason why transfer failed or None. """ return self._fail_reason - def set_failed(self, reason: str): + def set_failed(self, reason: str) -> None: """Mark progress as failed. Args: @@ -942,7 +945,7 @@ def get_transferred_size(self) -> int: """ return self._transferred - def set_transferred_size(self, transferred: int): + def set_transferred_size(self, transferred: int) -> None: """Set already transferred size in bytes. Args: @@ -955,7 +958,7 @@ def reset_transferred(self) -> None: """Reset transferred size to initial value.""" self._transferred = 0 - def add_transferred_chunk(self, chunk_size: int): + def add_transferred_chunk(self, chunk_size: int) -> None: """Add transferred chunk size in bytes. Args: @@ -978,7 +981,7 @@ def get_source_url(self) -> str: """ return self._source_url - def set_source_url(self, url: str): + def set_source_url(self, url: str) -> None: """Set source url from where transfer happens. Args: @@ -1000,7 +1003,7 @@ def get_destination_url(self) -> str: """ return self._destination_url - def set_destination_url(self, url: str): + def set_destination_url(self, url: str) -> None: """Set destination url where transfer happens. Args: @@ -1026,11 +1029,11 @@ def is_running(self) -> bool: return True @property - def transfer_progress(self) -> Optional[float]: + def transfer_progress(self) -> float | None: """Get transfer progress in percents. Returns: - Optional[float]: Transfer progress in percents or 'None' + float | None: Transfer progress in percents or 'None' if content size is unknown. """ @@ -1049,12 +1052,12 @@ def transfer_progress(self) -> Optional[float]: def create_dependency_package_basename( - platform_name: Optional[str] = None + platform_name: str | None = None ) -> str: """Create basename for dependency package file. Args: - platform_name (Optional[str]): Name of platform for which the + platform_name (str | None): Name of platform for which the bundle is targeted. Default value is current platform. Returns: @@ -1066,11 +1069,11 @@ def create_dependency_package_basename( now_date = datetime.datetime.now() time_stamp = now_date.strftime("%y%m%d%H%M") - return "ayon_{}_{}".format(time_stamp, platform_name) + return f"ayon_{time_stamp}_{platform_name}" -def _get_media_mime_type_from_ftyp(content: bytes) -> Optional[str]: +def _get_media_mime_type_from_ftyp(content: bytes) -> str | None: if content[8:10] == b"qt" or content[8:12] == b"MSNV": return "video/quicktime" @@ -1110,7 +1113,7 @@ def _get_media_mime_type_from_ftyp(content: bytes) -> Optional[str]: return None -def _get_media_mime_type_for_content_base(content: bytes) -> Optional[str]: +def _get_media_mime_type_for_content_base(content: bytes) -> str | None: """Determine Mime-Type of a file. Use header of the file to determine mime type (needs 12 bytes). @@ -1173,14 +1176,14 @@ def _get_media_mime_type_for_content_base(content: bytes) -> Optional[str]: return None -def _get_svg_mime_type(content: bytes) -> Optional[str]: +def _get_svg_mime_type(content: bytes) -> str | None: # SVG if b'xmlns="http://www.w3.org/2000/svg"' in content: return "image/svg+xml" return None -def _get_json_mime_type(content: bytes) -> Optional[str]: +def _get_json_mime_type(content: bytes) -> str | None: # json try: json.loads(content.decode("utf-8")) @@ -1190,14 +1193,14 @@ def _get_json_mime_type(content: bytes) -> Optional[str]: return None -def get_media_mime_type_for_content(content: bytes) -> Optional[str]: +def get_media_mime_type_for_content(content: bytes) -> str | None: mime_type = _get_media_mime_type_for_content_base(content) if mime_type is not None: return mime_type return _get_svg_mime_type(content) or _get_json_mime_type(content) -def get_media_mime_type_for_stream(stream: StreamType) -> Optional[str]: +def get_media_mime_type_for_stream(stream: StreamType) -> str | None: # Read only 12 bytes to determine mime type content = stream.read(12) mime_type = _get_media_mime_type_for_content_base(content) @@ -1208,14 +1211,14 @@ def get_media_mime_type_for_stream(stream: StreamType) -> Optional[str]: return _get_svg_mime_type(content) or _get_json_mime_type(content) -def get_media_mime_type(filepath: str) -> Optional[str]: +def get_media_mime_type(filepath: str) -> str | None: """Determine Mime-Type of a file. Args: filepath (str): Path to file. Returns: - Optional[str]: Mime type or None if is unknown mime type. + str | None: Mime type or None if is unknown mime type. """ if not filepath or not os.path.exists(filepath): From e8681fb91dbe85de00ee20d0adaa0ffdd3b26471 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 May 2026 17:04:50 +0200 Subject: [PATCH 421/506] fix typo --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 97c977a8d..91a8a596b 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -761,7 +761,7 @@ def validate_url( # Not sure if this is good idea? modified_url = stripperd_url.rstrip("/") - # Make sure url has http schema + # Make sure url has http scheme if not modified_url.lower().startswith("http"): modified_url = f"http://{modified_url}" From 3fb48a878238f23ace6296a300746b5239d22831 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 May 2026 17:05:10 +0200 Subject: [PATCH 422/506] use one variable for pathless url --- ayon_api/utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 91a8a596b..88ea037a3 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -779,10 +779,9 @@ def validate_url( hints=universal_hints ) + pathless_url = f"{parsed_url.scheme}://{parsed_url.netloc}" if parsed_url.path: - tmp_url = f"{parsed_url.scheme}://{parsed_url.netloc}" new_url = _try_connect_to_server( - tmp_url, timeout=timeout, verify=verify, cert=cert, @@ -801,8 +800,7 @@ def validate_url( hints = [] if parsed_url.path: - new_path = f"{parsed_url.scheme}{parsed_url.netloc}" - hints.append(f"did you mean \"{new_path}\"?") + hints.append(f"did you mean \"{pathless_url}\"?") raise UrlError( "Couldn't connect to server on \"{}\"".format(url), From 8f7b4955791bbe8aaeed97f9e2f78384431e5d65 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 4 May 2026 17:14:42 +0200 Subject: [PATCH 423/506] add 'UrlNotReached' to be raised for connection error --- ayon_api/exceptions.py | 16 ++++++++++++++-- ayon_api/utils.py | 6 ++++-- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/ayon_api/exceptions.py b/ayon_api/exceptions.py index 6a44e9d9c..c72beb915 100644 --- a/ayon_api/exceptions.py +++ b/ayon_api/exceptions.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import copy try: @@ -21,13 +23,23 @@ class UrlError(Exception): UI if needed. """ - def __init__(self, message, title, hints=None): + + def __init__( + self, + message: str, + title: str, + hints: list[str] | None = None, + ) -> None: if hints is None: hints = [] self.title = title self.hints = hints - super(UrlError, self).__init__(message) + super().__init__(message) + + +class UrlNotReached(UrlError): + pass class ServerError(Exception): diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 88ea037a3..cd3a9a71a 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -27,6 +27,7 @@ ) from .exceptions import ( UrlError, + UrlNotReached, ServerError, UnauthorizedError, HTTPRequestError, @@ -782,6 +783,7 @@ def validate_url( pathless_url = f"{parsed_url.scheme}://{parsed_url.netloc}" if parsed_url.path: new_url = _try_connect_to_server( + pathless_url, timeout=timeout, verify=verify, cert=cert, @@ -802,8 +804,8 @@ def validate_url( if parsed_url.path: hints.append(f"did you mean \"{pathless_url}\"?") - raise UrlError( - "Couldn't connect to server on \"{}\"".format(url), + raise UrlNotReached( + f"Couldn't connect to server on \"{url}\"", title="Couldn't connect to server", hints=hints + universal_hints ) From 044784092281bdf2f727814c910d1b51c6a30b5a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 May 2026 15:48:58 +0200 Subject: [PATCH 424/506] fix typo --- ayon_api/utils.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index cd3a9a71a..76c4509f0 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -751,8 +751,8 @@ def validate_url( UrlError: Error with short description and hints for user. """ - stripperd_url = url.strip() - if not stripperd_url: + stripped_url = url.strip() + if not stripped_url: raise UrlError( "Invalid url format. Url is empty.", title="Invalid url format", @@ -760,7 +760,7 @@ def validate_url( ) # Not sure if this is good idea? - modified_url = stripperd_url.rstrip("/") + modified_url = stripped_url.rstrip("/") # Make sure url has http scheme if not modified_url.lower().startswith("http"): From 888f8599836d4b970cb136e272b41299af3c235a Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 11 May 2026 14:40:50 +0000 Subject: [PATCH 425/506] Release version 1.2.17 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index a19f2ace4..fab1f87a0 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.17-dev" +__version__ = "1.2.17" diff --git a/pyproject.toml b/pyproject.toml index a131446fc..0d0f8f706 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.17-dev" +version = "1.2.17" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.17-dev" +version = "1.2.17" description = "AYON Python API" authors = [ "ynput.io " From 487f836bf02ea224306569146d54e28bf2f5c30d Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 11 May 2026 14:41:15 +0000 Subject: [PATCH 426/506] Bump version to 1.2.18-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index fab1f87a0..22244309c 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.17" +__version__ = "1.2.18-dev" diff --git a/pyproject.toml b/pyproject.toml index 0d0f8f706..9c37d9957 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.17" +version = "1.2.18-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.17" +version = "1.2.18-dev" description = "AYON Python API" authors = [ "ynput.io " From d2201cf1c622b2d81f9895a3c904b5a60fe6ea1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 11 May 2026 17:29:27 +0200 Subject: [PATCH 427/506] fix release --- .github/workflows/create_release.yml | 6 ++++-- .github/workflows/python-publish.yml | 17 +++++++++++++---- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index 89f9c1ea5..fc2e2b17b 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -16,6 +16,8 @@ on: jobs: release: runs-on: ubuntu-latest + outputs: + version: ${{ steps.get_version.outputs.version }} if: github.ref == 'refs/heads/develop' permissions: contents: write @@ -109,5 +111,5 @@ jobs: publish: needs: release uses: ./.github/workflows/python-publish.yml - secrets: - PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + with: + ref: ${{ needs.release.outputs.version }} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 6c282bcc9..2c83e91a6 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -9,12 +9,19 @@ name: ⬆️ Upload PyPi Package on: - release: - types: [published] + workflow_dispatch: + inputs: + ref: + description: 'Git ref (tag) to checkout for publishing' + required: true + type: string + workflow_call: - secrets: - PYPI_API_TOKEN: + inputs: + ref: + description: 'Git ref (tag) to checkout for publishing' required: true + type: string permissions: contents: read @@ -28,6 +35,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + ref: ${{ inputs.ref }} - name: Set up Python uses: actions/setup-python@v5 with: From 47ad0260a7e01c28702f685364c793a653f85058 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 May 2026 15:53:03 +0200 Subject: [PATCH 428/506] re-use variables --- ayon_api/graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index d377d94b0..2fb75c9e5 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -370,7 +370,7 @@ def query(self, con: ServerAPI) -> dict[str, Any]: variables = self.get_variables_values() response = con.query_graphql( query_str, - self.get_variables_values() + variables ) if response.errors: raise GraphQlQueryFailed(response.errors, query_str, variables) From 2c637eece7de1995d09d158f4f78ff69684a8433 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 14 May 2026 15:53:17 +0200 Subject: [PATCH 429/506] handle first and last properly --- ayon_api/graphql.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index 2fb75c9e5..33ba75413 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -304,7 +304,7 @@ def calculate_query(self) -> str: str: GraphQl string with variables and headers. Raises: - ValueError: Query has no fiels. + ValueError: Query has no fields. """ if not self._children: @@ -396,6 +396,7 @@ def continuous_query( while self.need_query: query_str = self.calculate_query() variables = self.get_variables_values() + response = con.query_graphql(query_str, variables) if response.errors: raise GraphQlQueryFailed( @@ -902,8 +903,12 @@ def parse_result( progress_data[cursor_key] = nodes_by_cursor page_info = value["pageInfo"] - new_cursor = page_info["endCursor"] - self._need_query = page_info["hasNextPage"] + if self._order == SortOrder.ascending: + new_cursor = page_info["endCursor"] + self._need_query = page_info["hasNextPage"] + else: + new_cursor = page_info["startCursor"] + self._need_query = page_info["hasPreviousPage"] edges = value["edges"] # Fake result parse if not edges: @@ -949,9 +954,7 @@ def _get_cursor_key(self) -> str: def get_filters(self) -> dict[str, Any]: filters = super().get_filters() - limit_key = "first" - if self._order == SortOrder.descending: - limit_key = "last" + limit_key = "first" if self._order == SortOrder.ascending else "last" limit_amount = 300 if self._limit: @@ -962,7 +965,10 @@ def get_filters(self) -> dict[str, Any]: filters[limit_key] = limit_amount if self._cursor: - filters["after"] = self._cursor + cursor_key = ( + "after" if self._order == SortOrder.ascending else "before" + ) + filters[cursor_key] = self._cursor return filters def calculate_query(self) -> str: @@ -1001,7 +1007,11 @@ def calculate_query(self) -> str: output.append(edges_offset + "pageInfo {") for page_key in ( "endCursor", - "hasNextPage", + ( + "hasNextPage" + if self._order == SortOrder.ascending + else "hasPreviousPage" + ), ): output.append(node_offset + page_key) output.append(edges_offset + "}") From bdacb71ab123501f37d09d2c33fa176dc9036475 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 May 2026 20:15:45 +0200 Subject: [PATCH 430/506] Fix truncation to 300 for `get_versions_links` due to shared cursor iterations --- ayon_api/graphql.py | 6 ++ tests/test_graphql_queries.py | 117 ++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index d377d94b0..afc9009c7 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -959,6 +959,12 @@ def get_filters(self) -> dict[str, Any]: if total > self._limit: limit_amount = self._limit - self._fetched_counter + if self.child_has_edges: + # Nested edge fields share a single cursor argument in the query. + # Query one parent item at a time so child pagination can't be + # overwritten by another parent from the same outer page. + limit_amount = min(limit_amount, 1) + filters[limit_key] = limit_amount if self._cursor: diff --git a/tests/test_graphql_queries.py b/tests/test_graphql_queries.py index 731ec5e8c..a275c693f 100644 --- a/tests/test_graphql_queries.py +++ b/tests/test_graphql_queries.py @@ -1,9 +1,12 @@ +from types import SimpleNamespace + import pytest from ayon_api.graphql import GraphQlQuery from ayon_api.graphql_queries import ( project_graphql_query, folders_graphql_query, + versions_graphql_query, ) from .conftest import project_name_fixture @@ -96,6 +99,120 @@ def test_get_variables_values(keys, values, types): assert query.get_variables_values() == expected +class DummyConnection: + def __init__(self, responses): + self._responses = list(responses) + self.calls = [] + + def query_graphql(self, query_str, variables): + self.calls.append((query_str, dict(variables))) + response = self._responses.pop(0) + return SimpleNamespace(errors=None, data={"data": response}) + + +def _version_link_edges(count, prefix): + return [ + {"id": f"{prefix}-{idx}"} + for idx in range(count) + ] + + +def test_nested_edge_pagination_queries_one_parent_at_a_time(): + query = versions_graphql_query({"id", "links.id"}) + query.set_variable_value("projectName", "test_project") + + con = DummyConnection([ + { + "project": { + "versions": { + "edges": [{ + "cursor": "version-1", + "node": { + "id": "version-1", + "links": { + "edges": _version_link_edges(300, "link-a"), + "pageInfo": { + "endCursor": "link-page-1", + "hasNextPage": True, + } + } + } + }], + "pageInfo": { + "endCursor": "versions-page-1", + "hasNextPage": True, + } + } + } + }, + { + "project": { + "versions": { + "edges": [{ + "cursor": "version-1", + "node": { + "id": "version-1", + "links": { + "edges": _version_link_edges(1, "link-b"), + "pageInfo": { + "endCursor": "link-page-2", + "hasNextPage": False, + } + } + } + }], + "pageInfo": { + "endCursor": "versions-page-1", + "hasNextPage": True, + } + } + } + }, + { + "project": { + "versions": { + "edges": [{ + "cursor": "version-2", + "node": { + "id": "version-2", + "links": { + "edges": _version_link_edges(1, "link-c"), + "pageInfo": { + "endCursor": "link-page-3", + "hasNextPage": False, + } + } + } + }], + "pageInfo": { + "endCursor": "versions-page-2", + "hasNextPage": False, + } + } + } + } + ]) + + output = query.query(con) + + versions = output["project"]["versions"] + assert len(versions) == 2 + assert versions[0]["id"] == "version-1" + assert len(versions[0]["links"]) == 301 + assert versions[0]["links"][0]["id"] == "link-a-0" + assert versions[0]["links"][-1]["id"] == "link-b-0" + assert versions[1]["id"] == "version-2" + assert versions[1]["links"] == [{"id": "link-c-0"}] + + first_query, second_query, third_query = [ + query_str for query_str, _variables in con.calls + ] + assert "versions(first: 1)" in first_query + assert 'links(first: 300)' in first_query + assert 'links(first: 300, after: "link-page-1")' in second_query + assert 'versions(first: 1, after: "versions-page-1")' in third_query + + """ def test_filtering(empty_query): assert empty_query._children == [] From 1255a732833e6a5a7ebf06c9213240562dc5d826 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 14 May 2026 20:33:42 +0200 Subject: [PATCH 431/506] Fix truncation to 300 for `get_versions_links` due to shared cursor iterations --- tests/test_graphql_queries.py | 117 ---------------------------------- 1 file changed, 117 deletions(-) diff --git a/tests/test_graphql_queries.py b/tests/test_graphql_queries.py index a275c693f..731ec5e8c 100644 --- a/tests/test_graphql_queries.py +++ b/tests/test_graphql_queries.py @@ -1,12 +1,9 @@ -from types import SimpleNamespace - import pytest from ayon_api.graphql import GraphQlQuery from ayon_api.graphql_queries import ( project_graphql_query, folders_graphql_query, - versions_graphql_query, ) from .conftest import project_name_fixture @@ -99,120 +96,6 @@ def test_get_variables_values(keys, values, types): assert query.get_variables_values() == expected -class DummyConnection: - def __init__(self, responses): - self._responses = list(responses) - self.calls = [] - - def query_graphql(self, query_str, variables): - self.calls.append((query_str, dict(variables))) - response = self._responses.pop(0) - return SimpleNamespace(errors=None, data={"data": response}) - - -def _version_link_edges(count, prefix): - return [ - {"id": f"{prefix}-{idx}"} - for idx in range(count) - ] - - -def test_nested_edge_pagination_queries_one_parent_at_a_time(): - query = versions_graphql_query({"id", "links.id"}) - query.set_variable_value("projectName", "test_project") - - con = DummyConnection([ - { - "project": { - "versions": { - "edges": [{ - "cursor": "version-1", - "node": { - "id": "version-1", - "links": { - "edges": _version_link_edges(300, "link-a"), - "pageInfo": { - "endCursor": "link-page-1", - "hasNextPage": True, - } - } - } - }], - "pageInfo": { - "endCursor": "versions-page-1", - "hasNextPage": True, - } - } - } - }, - { - "project": { - "versions": { - "edges": [{ - "cursor": "version-1", - "node": { - "id": "version-1", - "links": { - "edges": _version_link_edges(1, "link-b"), - "pageInfo": { - "endCursor": "link-page-2", - "hasNextPage": False, - } - } - } - }], - "pageInfo": { - "endCursor": "versions-page-1", - "hasNextPage": True, - } - } - } - }, - { - "project": { - "versions": { - "edges": [{ - "cursor": "version-2", - "node": { - "id": "version-2", - "links": { - "edges": _version_link_edges(1, "link-c"), - "pageInfo": { - "endCursor": "link-page-3", - "hasNextPage": False, - } - } - } - }], - "pageInfo": { - "endCursor": "versions-page-2", - "hasNextPage": False, - } - } - } - } - ]) - - output = query.query(con) - - versions = output["project"]["versions"] - assert len(versions) == 2 - assert versions[0]["id"] == "version-1" - assert len(versions[0]["links"]) == 301 - assert versions[0]["links"][0]["id"] == "link-a-0" - assert versions[0]["links"][-1]["id"] == "link-b-0" - assert versions[1]["id"] == "version-2" - assert versions[1]["links"] == [{"id": "link-c-0"}] - - first_query, second_query, third_query = [ - query_str for query_str, _variables in con.calls - ] - assert "versions(first: 1)" in first_query - assert 'links(first: 300)' in first_query - assert 'links(first: 300, after: "link-page-1")' in second_query - assert 'versions(first: 1, after: "versions-page-1")' in third_query - - """ def test_filtering(empty_query): assert empty_query._children == [] From 422e2f11b9c3ab9748a9a2875be75d87a95c774b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 09:30:12 +0200 Subject: [PATCH 432/506] check if cursor is repeated --- ayon_api/exceptions.py | 6 +++++- ayon_api/graphql.py | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ayon_api/exceptions.py b/ayon_api/exceptions.py index c72beb915..513eb88e4 100644 --- a/ayon_api/exceptions.py +++ b/ayon_api/exceptions.py @@ -78,7 +78,11 @@ class HTTPRequestError(RequestError): pass -class GraphQlQueryFailed(Exception): +class GraphQlQueryError(Exception): + pass + + +class GraphQlQueryFailed(GraphQlQueryError): def __init__(self, errors, query, variables): if variables is None: variables = {} diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index 33ba75413..af5488c61 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -6,7 +6,7 @@ import typing from typing import Optional, Iterable, Any, Generator -from .exceptions import GraphQlQueryFailed +from .exceptions import GraphQlQueryError, GraphQlQueryFailed from .utils import SortOrder if typing.TYPE_CHECKING: @@ -947,6 +947,11 @@ def parse_result( if change_cursor: for child in self._children_iter(): child.reset_cursor() + if new_cursor == self._cursor: + raise GraphQlQueryError( + "Cursor didn't change during pagination." + " This can cause infinite loop." + ) self._cursor = new_cursor def _get_cursor_key(self) -> str: From cec715125dbd737b83d1ef908b9576d6a319fd73 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 12:18:50 +0200 Subject: [PATCH 433/506] change how cursor is changed --- ayon_api/graphql.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index af5488c61..e195d9dd7 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -909,6 +909,7 @@ def parse_result( else: new_cursor = page_info["startCursor"] self._need_query = page_info["hasPreviousPage"] + edges = value["edges"] # Fake result parse if not edges: @@ -936,22 +937,14 @@ def parse_result( for child in self._children: child.parse_result(edge["node"], edge_value, progress_data) - if not self._need_query: - return - change_cursor = True for child in self._children_iter(): if child.need_query: change_cursor = False - if change_cursor: + if change_cursor and self._need_query: for child in self._children_iter(): child.reset_cursor() - if new_cursor == self._cursor: - raise GraphQlQueryError( - "Cursor didn't change during pagination." - " This can cause infinite loop." - ) self._cursor = new_cursor def _get_cursor_key(self) -> str: From fd3e50652a3dbf49ac10312221edd72b27b48443 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 12:20:14 +0200 Subject: [PATCH 434/506] add check of cursor back --- ayon_api/graphql.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index e195d9dd7..1f079bc71 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -943,6 +943,12 @@ def parse_result( change_cursor = False if change_cursor and self._need_query: + if new_cursor == self._cursor: + raise GraphQlQueryError( + "Cursor didn't change during pagination." + " This can cause infinite loop." + ) + for child in self._children_iter(): child.reset_cursor() self._cursor = new_cursor From ed18e4508a8e6095203b4e509f712638cb34249f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 12:21:10 +0200 Subject: [PATCH 435/506] Use explicit 1 --- ayon_api/graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index afc9009c7..a0b872bec 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -963,7 +963,7 @@ def get_filters(self) -> dict[str, Any]: # Nested edge fields share a single cursor argument in the query. # Query one parent item at a time so child pagination can't be # overwritten by another parent from the same outer page. - limit_amount = min(limit_amount, 1) + limit_amount = 1 filters[limit_key] = limit_amount From fea45aa313f86b2b552175998fe6191c33d4c62b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 12:22:37 +0200 Subject: [PATCH 436/506] use correct cursor key --- ayon_api/graphql.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index 1f079bc71..8e762eb3d 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -1010,7 +1010,11 @@ def calculate_query(self) -> str: # Add page information output.append(edges_offset + "pageInfo {") for page_key in ( - "endCursor", + ( + "endCursor" + if self._order == SortOrder.ascending + else "startCursor" + ), ( "hasNextPage" if self._order == SortOrder.ascending From 5a80083ca582ef28c5512c561d3bffcc8161b11f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 13:47:33 +0200 Subject: [PATCH 437/506] change how 'get_tasks_by_folder_paths' works --- ayon_api/_api_helpers/tasks.py | 76 ++++++++++++++-------------------- 1 file changed, 31 insertions(+), 45 deletions(-) diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index 4ffa2dd5f..8067d49a5 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -236,29 +236,21 @@ def get_tasks_by_folder_paths( folder path. """ - folder_paths = set(folder_paths) - if not project_name or not folder_paths: - return {} - - graphql_filters = { - "projectName": project_name, - "folderPaths": list(folder_paths), + output = { + folder_path: [] + for folder_path in folder_paths + } + if not project_name or not output: + return output + + folder_path_by_id = { + folder["id"]: folder["path"] + for folder in self.get_folders( + project_name, + folder_paths=output.keys(), + fields={"id", "path"}, + ) } - - if not prepare_list_filters( - graphql_filters, - ("taskNames", task_names), - ("taskTypes", task_types), - ("taskAssigneesAny", assignees), - ("taskAssigneesAll", assignees_all), - ("taskStatuses", statuses), - ("taskTags", tags), - ): - return {} - - filters = self._prepare_advanced_filters(filters) - if filters: - graphql_filters["filter"] = filters if not fields: fields = self.get_default_fields_for_type("task") @@ -266,31 +258,25 @@ def get_tasks_by_folder_paths( fields = set(fields) self._prepare_fields("task", fields, own_attributes) - if active is not None: - fields.add("active") - - self._prepare_link_fields(fields) - - query = tasks_by_folder_paths_graphql_query(fields) - for attr, filter_value in graphql_filters.items(): - query.set_variable_value(attr, filter_value) + fields.add("folderId") - output = { - folder_path: [] - for folder_path in folder_paths - } - for parsed_data in query.continuous_query(self): - for folder in parsed_data["project"]["folders"]: - folder_path = folder["path"] - for task in folder["tasks"]: - if active is not None and active is not task["active"]: - continue - - self._convert_entity_data(task) + for task_entity in self.get_tasks( + project_name, + folder_ids=folder_path_by_id.keys(), + task_names=task_names, + task_types=task_types, + assignees=assignees, + assignees_all=assignees_all, + statuses=statuses, + tags=tags, + active=active, + filters=filters, + fields=fields, + ): + folder_id = task_entity["folderId"] + folder_path = folder_path_by_id[folder_id] + output[folder_path].append(task_entity) - if own_attributes: - fill_own_attribs(task) - output[folder_path].append(task) return output def get_tasks_by_folder_path( From b7fe3e3da3197c1864b38a49a3e6ea9b2915f529 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 13:48:04 +0200 Subject: [PATCH 438/506] remove unused import --- ayon_api/_api_helpers/tasks.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index 8067d49a5..a404435bf 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -11,7 +11,6 @@ ) from ayon_api.graphql_queries import ( tasks_graphql_query, - tasks_by_folder_paths_graphql_query, ) from .base import BaseServerAPI From 600b40fc6a3f6c605826600cd24a98a203460278 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 13:50:23 +0200 Subject: [PATCH 439/506] remove 'tasks_by_folder_paths_graphql_query' --- ayon_api/graphql_queries.py | 50 ------------------------------------- 1 file changed, 50 deletions(-) diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index d376d0d43..c3f85e977 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -243,56 +243,6 @@ def tasks_graphql_query(fields: set[str]) -> GraphQlQuery: return query -def tasks_by_folder_paths_graphql_query(fields: set[str]) -> GraphQlQuery: - query = GraphQlQuery("TasksByFolderPathQuery") - project_name_var = query.add_variable("projectName", "String!") - task_names_var = query.add_variable("taskNames", "[String!]") - task_types_var = query.add_variable("taskTypes", "[String!]") - folder_paths_var = query.add_variable("folderPaths", "[String!]") - assignees_any_var = query.add_variable("taskAssigneesAny", "[String!]") - assignees_all_var = query.add_variable("taskAssigneesAll", "[String!]") - statuses_var = query.add_variable("taskStatuses", "[String!]") - tags_var = query.add_variable("taskTags", "[String!]") - filter_var = query.add_variable("filter", "String!") - - project_field = query.add_field("project") - project_field.set_filter("name", project_name_var) - - folders_field = project_field.add_field_with_edges("folders") - folders_field.add_field("path") - folders_field.set_filter("paths", folder_paths_var) - - tasks_field = folders_field.add_field_with_edges("tasks") - # WARNING: At the moment when this been created 'names' filter - # is not supported - tasks_field.set_filter("names", task_names_var) - tasks_field.set_filter("taskTypes", task_types_var) - tasks_field.set_filter("assigneesAny", assignees_any_var) - tasks_field.set_filter("assignees", assignees_all_var) - tasks_field.set_filter("statuses", statuses_var) - tasks_field.set_filter("tags", tags_var) - tasks_field.set_filter("filter", filter_var) - - nested_fields = fields_to_dict(fields) - - add_links_fields(tasks_field, nested_fields) - - query_queue = collections.deque() - for key, value in nested_fields.items(): - query_queue.append((key, value, tasks_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 - - def products_graphql_query(fields: set[str]) -> GraphQlQuery: query = GraphQlQuery("ProductsQuery") From a0b91efc6ac2a27e2f5931810a505ff30f36d6f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 13:58:44 +0200 Subject: [PATCH 440/506] single line import --- ayon_api/_api_helpers/tasks.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ayon_api/_api_helpers/tasks.py b/ayon_api/_api_helpers/tasks.py index a404435bf..1cae5caa5 100644 --- a/ayon_api/_api_helpers/tasks.py +++ b/ayon_api/_api_helpers/tasks.py @@ -9,9 +9,7 @@ create_entity_id, NOT_SET, ) -from ayon_api.graphql_queries import ( - tasks_graphql_query, -) +from ayon_api.graphql_queries import tasks_graphql_query from .base import BaseServerAPI From 3c2f2bbd9de2aabc8dad5dfc519b0612743857af Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 15 May 2026 12:20:42 +0000 Subject: [PATCH 441/506] Release version 1.2.18 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 22244309c..46ac47ff9 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.18-dev" +__version__ = "1.2.18" diff --git a/pyproject.toml b/pyproject.toml index 9c37d9957..5171b23e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.18-dev" +version = "1.2.18" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.18-dev" +version = "1.2.18" description = "AYON Python API" authors = [ "ynput.io " From aa2ae33c998d3a959a56a91096c20a70d0ea8acf Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 15 May 2026 12:21:05 +0000 Subject: [PATCH 442/506] Bump version to 1.2.19-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 46ac47ff9..c2e0094ea 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.18" +__version__ = "1.2.19-dev" diff --git a/pyproject.toml b/pyproject.toml index 5171b23e9..6ea570911 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.18" +version = "1.2.19-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.18" +version = "1.2.19-dev" description = "AYON Python API" authors = [ "ynput.io " From 74d43d92bf50bd719b3a27975f104e1a28885a98 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 15 May 2026 14:34:28 +0200 Subject: [PATCH 443/506] fix CI action --- .github/workflows/create_release.yml | 4 ++++ .github/workflows/python-publish.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml index fc2e2b17b..59d5f67f4 100644 --- a/.github/workflows/create_release.yml +++ b/.github/workflows/create_release.yml @@ -111,5 +111,9 @@ jobs: publish: needs: release uses: ./.github/workflows/python-publish.yml + permissions: + contents: read + id-token: write + secrets: inherit with: ref: ${{ needs.release.outputs.version }} diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 2c83e91a6..929b67f09 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -22,9 +22,13 @@ on: description: 'Git ref (tag) to checkout for publishing' required: true type: string + secrets: + PYPI_API_TOKEN: + required: false permissions: contents: read + id-token: write jobs: deploy: From bb48d2f600be405f648f9f141a13837430f69ece Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 15 May 2026 12:35:11 +0000 Subject: [PATCH 444/506] Release version 1.2.19 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index c2e0094ea..196c5aeca 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.19-dev" +__version__ = "1.2.19" diff --git a/pyproject.toml b/pyproject.toml index 6ea570911..e0cecf3f6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.19-dev" +version = "1.2.19" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.19-dev" +version = "1.2.19" description = "AYON Python API" authors = [ "ynput.io " From 988cf8283821b88e789c4d70388ad6d33ba85af2 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 15 May 2026 12:35:34 +0000 Subject: [PATCH 445/506] Bump version to 1.2.20-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 196c5aeca..138b8c413 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.19" +__version__ = "1.2.20-dev" diff --git a/pyproject.toml b/pyproject.toml index e0cecf3f6..51c92deb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.19" +version = "1.2.20-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.19" +version = "1.2.20-dev" description = "AYON Python API" authors = [ "ynput.io " From 28707bda3432597138756f37cfdfa70e5ae438be Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 May 2026 16:18:16 +0200 Subject: [PATCH 446/506] remove icon and color from product type fields --- ayon_api/constants.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ayon_api/constants.py b/ayon_api/constants.py index c122e4eef..8cee22613 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -75,15 +75,14 @@ # --- Product types --- DEFAULT_PRODUCT_TYPE_FIELDS = { "name", - "icon", - "color", } # --- Product base type --- DEFAULT_PRODUCT_BASE_TYPE_FIELDS = { - # Ignore 'icon' and 'color' - # - current server implementation always returns 'null' + # TODO add 'icon' and 'color' when server supports it "name", + # "icon", + # "color", } # --- Project --- From f863726411f03206364bb1a1c2544613c71fd582 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 May 2026 16:18:43 +0200 Subject: [PATCH 447/506] added stagingBundle to bundle dict type --- ayon_api/typing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 68394c046..f8016524f 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -235,6 +235,7 @@ class BundleInfoDict(TypedDict): class BundlesInfoDict(TypedDict): bundles: list[BundleInfoDict] productionBundle: str + stagingBundle: str devBundles: list[str] From 52dfe9bc469cc2604996e6a042c909b33bd39275 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 25 May 2026 16:57:17 +0200 Subject: [PATCH 448/506] pass include_skeleton --- ayon_api/_api_helpers/projects.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 0cadfeef8..51c501176 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -364,6 +364,7 @@ def get_project( graphql_project = next(self._get_graphql_projects( None, None, + include_skeleton=True, project_name=project_name, fields=graphql_fields, own_attributes=own_attributes, From e71f2b643bfbc38ae746ffcbf0635d964255b707 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 May 2026 11:25:03 +0200 Subject: [PATCH 449/506] use set for assignees --- ayon_api/entity_hub.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index 09791d90d..942fa60ea 100644 --- a/ayon_api/entity_hub.py +++ b/ayon_api/entity_hub.py @@ -56,6 +56,15 @@ class ProjectStatusDict(TypedDict): scope: NotRequired[Optional[StatusEntityType]] +class Assignees(set): + """Helper class for task assignees. + + Assignees used to be a list and 'append' is being used a lot. + """ + def append(self, item: str) -> None: + self.add(item) + + class _CustomNone: def __init__(self, name: Optional[str] = None) -> None: self._name = name or "CustomNone" @@ -3430,9 +3439,9 @@ def __init__( entity_hub=entity_hub, ) if assignees is None: - assignees = [] + assignees = Assignees() else: - assignees = list(assignees) + assignees = Assignees(assignees) self._task_type = task_type self._assignees = assignees @@ -3463,11 +3472,11 @@ def set_task_type(self, task_type: str) -> None: task_type = property(get_task_type, set_task_type) - def get_assignees(self) -> list[str]: + def get_assignees(self) -> Assignees[str]: """Task assignees. Returns: - list[str]: Task assignees. + Assignees[str]: Task assignees. """ return self._assignees @@ -3479,7 +3488,7 @@ def set_assignees(self, assignees: Iterable[str]) -> None: assignees (Iterable[str]): assignees. """ - self._assignees = list(assignees) + self._assignees = Assignees(assignees) assignees = property(get_assignees, set_assignees) @@ -3497,7 +3506,7 @@ def changes(self) -> dict[str, Any]: changes["taskType"] = self._task_type if self._orig_assignees != self._assignees: - changes["assignees"] = self._assignees + changes["assignees"] = list(self._assignees) return changes @@ -3549,7 +3558,7 @@ def to_create_body_data(self) -> dict[str, Any]: output["tags"] = self.tags if self.assignees: - output["assignees"] = self.assignees + output["assignees"] = list(self.assignees) if self._data is not UNKNOWN_VALUE: output["data"] = self._data.get_new_entity_value() From 52e9a6f3f368d44795974643a882bbe9e4c44569 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 26 May 2026 09:48:05 +0000 Subject: [PATCH 450/506] Release version 1.2.20 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 138b8c413..9a847a8f5 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.20-dev" +__version__ = "1.2.20" diff --git a/pyproject.toml b/pyproject.toml index 51c92deb7..23c31e5e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.20-dev" +version = "1.2.20" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.20-dev" +version = "1.2.20" description = "AYON Python API" authors = [ "ynput.io " From fa5e761e784574388bd1608ccc718ec802742f9b Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 26 May 2026 09:48:27 +0000 Subject: [PATCH 451/506] Bump version to 1.2.21-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 9a847a8f5..5c0d0e7c3 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.20" +__version__ = "1.2.21-dev" diff --git a/pyproject.toml b/pyproject.toml index 23c31e5e8..470293c4d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.20" +version = "1.2.21-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.20" +version = "1.2.21-dev" description = "AYON Python API" authors = [ "ynput.io " From 9cef560094ee34f4136c7511d83d61b4c1bfc281 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 May 2026 12:13:41 +0200 Subject: [PATCH 452/506] add icon and color for productBaseTypes files --- ayon_api/_api_helpers/projects.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 51c501176..c17068816 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -805,6 +805,10 @@ def _get_project_graphql_fields( for f_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS: graphql_fields.add(f"{field}.{f_name}") + if self.get_server_version_tuple() > (1, 15, 3): + graphql_fields.add("productBaseTypes.icon") + graphql_fields.add("productBaseTypes.color") + elif field.startswith("productBaseTypes"): must_use_graphql = True graphql_fields.add(field) From cb55994ecc084637def3fd0bbd78ffdc0f705f61 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 May 2026 18:57:19 +0200 Subject: [PATCH 453/506] use graphql schema to figure out what fields are fetched --- ayon_api/_api_helpers/projects.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index c17068816..4e6fbb20b 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -50,6 +50,8 @@ class ProjectFetchType(Enum): class ProjectsAPI(BaseServerAPI): + _project_product_base_type_fields = None + def get_project_anatomy_presets(self) -> list[AnatomyPresetDict]: """Anatomy presets available on server. @@ -802,13 +804,9 @@ def _get_project_graphql_fields( elif field == "productBaseTypes": must_use_graphql = True fields.discard(field) - for f_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS: + for f_name in self._get_project_product_base_type_fields(): graphql_fields.add(f"{field}.{f_name}") - if self.get_server_version_tuple() > (1, 15, 3): - graphql_fields.add("productBaseTypes.icon") - graphql_fields.add("productBaseTypes.color") - elif field.startswith("productBaseTypes"): must_use_graphql = True graphql_fields.add(field) @@ -1021,3 +1019,17 @@ def _get_project_roots_values( ) response.raise_for_status() return response.data + + def _get_project_product_base_type_fields(self) -> set[str]: + if self._project_product_base_type_fields is not None: + return self._project_product_base_type_fields + + graphql_schema = self.get_graphql_schema() + + field_names = {"name"} + for type_def in graphql_schema["__schema"]["types"]: + if type_def["name"] == "ProductBaseType": + field_names = {field["name"] for field in type_def["fields"]} + break + self._project_product_base_type_fields = field_names + return field_names \ No newline at end of file From 9408d847186b81eaccf96f6619301060efcfb7a2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 26 May 2026 19:00:31 +0200 Subject: [PATCH 454/506] remove unused import --- ayon_api/_api_helpers/projects.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 4e6fbb20b..df602d808 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -9,7 +9,6 @@ from ayon_api.constants import ( PROJECT_NAME_REGEX, - DEFAULT_PRODUCT_BASE_TYPE_FIELDS, DEFAULT_PRODUCT_TYPE_FIELDS, ) from ayon_api.utils import prepare_query_string, fill_own_attribs @@ -804,6 +803,7 @@ def _get_project_graphql_fields( elif field == "productBaseTypes": must_use_graphql = True fields.discard(field) + # for f_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS: for f_name in self._get_project_product_base_type_fields(): graphql_fields.add(f"{field}.{f_name}") @@ -1032,4 +1032,4 @@ def _get_project_product_base_type_fields(self) -> set[str]: field_names = {field["name"] for field in type_def["fields"]} break self._project_product_base_type_fields = field_names - return field_names \ No newline at end of file + return field_names From 5e8d1abcff2ff908e57c92cc32bbadfbffa41cf4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:43:21 +0200 Subject: [PATCH 455/506] capture 401 earlier --- ayon_api/server_api.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7b60d68a7..73c45c8f3 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -765,6 +765,7 @@ def validate_token(self) -> bool: except UnauthorizedError: self._token_is_valid = False + self.close_session() finally: self._token_validation_started = False @@ -1222,6 +1223,11 @@ def _do_rest_request(self, function, url, **kwargs): ): self.validate_token() + if self._token_is_valid is False: + raise UnauthorizedError( + "Authentication token was invalidated." + ) + if "headers" not in kwargs: kwargs["headers"] = self.get_headers() @@ -1297,6 +1303,14 @@ def _do_rest_request(self, function, url, **kwargs): if new_response is not None: return new_response + if ( + response is not None + and self._token_is_valid + and response.status_code == 401 + ): + self._token_is_valid = False + self.close_session() + new_response = RestApiResponse(response) self.log.debug(f"Response {str(new_response)}") return new_response From 9a74717948595fb8b06ec6c7ac7f4d83a2f0d27b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:43:39 +0200 Subject: [PATCH 456/506] fix return statement in finally --- ayon_api/server_api.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 73c45c8f3..cdabfb268 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -187,9 +187,8 @@ def as_user(self, username: Optional[str]) -> Generator[None, None, None]: user_id = uuid.uuid4().hex self._user_ids.append(user_id) self._users_by_id[user_id] = username - try: - yield - finally: + + def _cleanup(): self._users_by_id.pop(user_id, None) if not self._user_ids: return @@ -208,6 +207,11 @@ def as_user(self, username: Optional[str]) -> Generator[None, None, None]: new_last_user = self._users_by_id.get(self._user_ids[-1]) self._last_user = new_last_user + try: + yield + finally: + _cleanup() + class ServerAPI( InstallersAPI, From c60e8b5da8e27685b7c1ffdf684f3fc2ee7d6a1d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 13:47:19 +0200 Subject: [PATCH 457/506] support cert and verify in token validation functions --- ayon_api/utils.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index d2cb88570..d486db3cf 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -658,6 +658,9 @@ def logout_from_server( def get_user_by_token( url: str, token: str, + *, + verify: str | bool | None = None, + cert: str | None = None, timeout: float | None = None, ) -> dict[str, Any] | None: """Get user information by url and token. @@ -675,6 +678,12 @@ def get_user_by_token( if timeout is None: timeout = get_default_timeout() + if verify is None: + verify = os.environ.get("AYON_CA_FILE") or True + + if cert is None: + cert = os.environ.get("AYON_CERT_FILE") or None + base_headers = { "Content-Type": "application/json", } @@ -688,6 +697,8 @@ def get_user_by_token( f"{url}/api/users/me", headers=headers, timeout=timeout, + verify=verify, + cert=cert, ) if response.status_code == 200: return response.json() @@ -697,6 +708,9 @@ def get_user_by_token( def is_token_valid( url: str, token: str, + *, + verify: str | bool | None = None, + cert: str | None = None, timeout: float | None = None, ) -> bool: """Check if token is valid. @@ -713,7 +727,13 @@ def is_token_valid( bool: True if token is valid. """ - if get_user_by_token(url, token, timeout=timeout): + if get_user_by_token( + url, + token, + verify=verify, + cert=cert, + timeout=timeout + ): return True return False From 557c5b69f1a07a2aaa2cc46dfb779ae92e72a450 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:10:24 +0200 Subject: [PATCH 458/506] fix token validation --- ayon_api/server_api.py | 49 +++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index cdabfb268..487214116 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -67,6 +67,7 @@ get_media_mime_type_for_stream, get_machine_name, fill_own_attribs, + is_token_valid, ) from ._api_helpers import ( InstallersAPI, @@ -758,19 +759,27 @@ def validate_server_availability(self): ) def validate_token(self) -> bool: + if self._access_token is None: + self._token_is_valid = False + self.close_session() + return False + + self._token_validation_started = True try: - self._token_validation_started = True # TODO add other possible validations # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info - self.get_info() - self.get_user() - self._token_is_valid = True - - except UnauthorizedError: + self._get_server_info() + self._token_is_valid = is_token_valid( + self.base_url, + self._access_token, + verify=self._ssl_verify, + cert=self._cert + ) + except Exception: self._token_is_valid = False self.close_session() - + self.log.error("Failed to validate token.", exc_info=True) finally: self._token_validation_started = False return self._token_is_valid @@ -866,6 +875,9 @@ def get_info(self) -> dict[str, Any]: dict[str, Any]: Information from server. """ + if self._session is None: + return self._get_server_info() + response = self.get("info") response.raise_for_status() return response.data @@ -1213,10 +1225,19 @@ def _logout(self): logout_from_server(self._base_url, self._access_token) def _do_rest_request(self, function, url, **kwargs): + if ( + self._session is not None + and self._token_is_valid is False + ): + raise UnauthorizedError( + "Authentication token was invalidated." + ) + kwargs.setdefault("timeout", self.timeout) max_retries = kwargs.get("max_retries", self.max_retries) if max_retries < 1: max_retries = 1 + if self._session is None: # Validate token if was not yet validated # - ignore validation if we're in middle of @@ -1227,11 +1248,6 @@ def _do_rest_request(self, function, url, **kwargs): ): self.validate_token() - if self._token_is_valid is False: - raise UnauthorizedError( - "Authentication token was invalidated." - ) - if "headers" not in kwargs: kwargs["headers"] = self.get_headers() @@ -1573,6 +1589,15 @@ def _endpoint_to_url( base_url = self._rest_url if use_rest else self._base_url return f"{base_url}/{endpoint}" + def _get_server_info(self) -> dict[str, Any]: + """Get server info without a session.""" + response = requests.get( + f"{self._rest_url}/info", + cert=self._cert, + verify=self._ssl_verify, + ) + response.raise_for_status() + return response.json() def _download_file_to_stream( self, endpoint: str, From ce3a33f11a7ed020c5fe74ca72de3d4cf8f891a9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 14:10:54 +0200 Subject: [PATCH 459/506] move private methods below public ones --- ayon_api/server_api.py | 273 ++++++++++++++++++++--------------------- 1 file changed, 136 insertions(+), 137 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 487214116..d9ed5faa8 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -951,29 +951,6 @@ def links_graphql_support_data(self) -> bool: ) return self._links_graphql_support_data - def _get_user_info(self) -> Optional[dict[str, Any]]: - if self._access_token is None: - return None - - if self._access_token_is_service is not None: - response = self.get("users/me") - if response.status == 200: - return response.data - return None - - self._access_token_is_service = False - response = self.get("users/me") - if response.status == 200: - return response.data - - self._access_token_is_service = True - response = self.get("users/me") - if response.status == 200: - return response.data - - self._access_token_is_service = None - return None - def get_users( self, project_name: Optional[str] = None, @@ -1221,120 +1198,6 @@ def logout(self, soft: bool = False): self._logout() self.reset_token() - def _logout(self): - logout_from_server(self._base_url, self._access_token) - - def _do_rest_request(self, function, url, **kwargs): - if ( - self._session is not None - and self._token_is_valid is False - ): - raise UnauthorizedError( - "Authentication token was invalidated." - ) - - kwargs.setdefault("timeout", self.timeout) - max_retries = kwargs.get("max_retries", self.max_retries) - if max_retries < 1: - max_retries = 1 - - if self._session is None: - # Validate token if was not yet validated - # - ignore validation if we're in middle of - # validation - if ( - self._token_is_valid is None - and not self._token_validation_started - ): - self.validate_token() - - if "headers" not in kwargs: - kwargs["headers"] = self.get_headers() - - if isinstance(function, RequestType): - function = self._base_functions_mapping[function] - - elif isinstance(function, RequestType): - function = self._session_functions_mapping[function] - - response = None - new_response = None - for retry_idx in reversed(range(max_retries)): - try: - response = function(url, **kwargs) - - # Usually these mean, try later. - # 502: returned by the proxy: nginx - # 503: returned by the server: if no capacity - if response.status_code in {502, 503}: - new_response = RestApiResponse(response) - self.log.warning( - "Server returned %s status code." - " Retrying with longer delay...", - response.status_code - ) - if retry_idx != 0: - time.sleep(2) - continue - break - - except ConnectionRefusedError: - if retry_idx == 0: - self.log.warning( - "Connection error happened.", exc_info=True - ) - - # Server may be restarting - new_response = RestApiResponse( - None, - { - "detail": ( - "Unable to connect the server. Connection refused" - ) - } - ) - - except requests.exceptions.Timeout: - # Connection timed out - new_response = RestApiResponse( - None, - {"detail": "Connection timed out."} - ) - - except requests.exceptions.ConnectionError: - # Log warning only on last attempt - if retry_idx == 0: - self.log.warning( - "Connection error happened.", exc_info=True - ) - - new_response = RestApiResponse( - None, - { - "detail": ( - "Unable to connect the server. Connection error" - ) - } - ) - - if retry_idx != 0: - time.sleep(0.1) - - if new_response is not None: - return new_response - - if ( - response is not None - and self._token_is_valid - and response.status_code == 401 - ): - self._token_is_valid = False - self.close_session() - - new_response = RestApiResponse(response) - self.log.debug(f"Response {str(new_response)}") - return new_response - def raw_post(self, entrypoint: str, **kwargs): url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [POST] {url}") @@ -1589,6 +1452,9 @@ def _endpoint_to_url( base_url = self._rest_url if use_rest else self._base_url return f"{base_url}/{endpoint}" + def _logout(self): + logout_from_server(self._base_url, self._access_token) + def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" response = requests.get( @@ -1598,6 +1464,139 @@ def _get_server_info(self) -> dict[str, Any]: ) response.raise_for_status() return response.json() + + def _get_user_info(self) -> Optional[dict[str, Any]]: + if self._access_token is None: + return None + + if self._access_token_is_service is not None: + response = self.get("users/me") + if response.status == 200: + return response.data + return None + + self._access_token_is_service = False + response = self.get("users/me") + if response.status == 200: + return response.data + + self._access_token_is_service = True + response = self.get("users/me") + if response.status == 200: + return response.data + + self._access_token_is_service = None + return None + + def _do_rest_request(self, function, url, **kwargs): + kwargs.setdefault("timeout", self.timeout) + max_retries = kwargs.get("max_retries", self.max_retries) + if max_retries < 1: + max_retries = 1 + + if self._token_is_valid is False: + raise UnauthorizedError( + "Authentication token was invalidated." + ) + + if self._session is None: + # Validate token if was not yet validated + # - ignore validation if we're in middle of + # validation + if ( + self._token_is_valid is None + and not self._token_validation_started + ): + self.validate_token() + + if "headers" not in kwargs: + kwargs["headers"] = self.get_headers() + + if isinstance(function, RequestType): + function = self._base_functions_mapping[function] + + elif isinstance(function, RequestType): + function = self._session_functions_mapping[function] + + response = None + new_response = None + for retry_idx in reversed(range(max_retries)): + try: + response = function(url, **kwargs) + + # Usually these mean, try later. + # 502: returned by the proxy: nginx + # 503: returned by the server: if no capacity + if response.status_code in {502, 503}: + new_response = RestApiResponse(response) + self.log.warning( + "Server returned %s status code." + " Retrying with longer delay...", + response.status_code + ) + if retry_idx != 0: + time.sleep(2) + continue + break + + except ConnectionRefusedError: + if retry_idx == 0: + self.log.warning( + "Connection error happened.", exc_info=True + ) + + # Server may be restarting + new_response = RestApiResponse( + None, + { + "detail": ( + "Unable to connect the server. Connection refused" + ) + } + ) + + except requests.exceptions.Timeout: + # Connection timed out + new_response = RestApiResponse( + None, + {"detail": "Connection timed out."} + ) + + except requests.exceptions.ConnectionError: + # Log warning only on last attempt + if retry_idx == 0: + self.log.warning( + "Connection error happened.", exc_info=True + ) + + new_response = RestApiResponse( + None, + { + "detail": ( + "Unable to connect the server. Connection error" + ) + } + ) + + if retry_idx != 0: + time.sleep(0.1) + + if new_response is not None: + return new_response + + if ( + response is not None + and self._token_is_valid + and response.status_code == 401 + ): + self._token_is_valid = False + self.close_session() + self._trigger_on_invalidate_callbacks() + + new_response = RestApiResponse(response) + self.log.debug(f"Response {str(new_response)}") + return new_response + def _download_file_to_stream( self, endpoint: str, From b80b51ac2044c39290ef95ec3d92525faabba00e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:05:39 +0200 Subject: [PATCH 460/506] handle if user service or not with utils function 'get_user_info_by_token' --- ayon_api/server_api.py | 11 +++-- ayon_api/utils.py | 94 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d9ed5faa8..7a6131566 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -67,7 +67,7 @@ get_media_mime_type_for_stream, get_machine_name, fill_own_attribs, - is_token_valid, + get_user_info_by_token, ) from ._api_helpers import ( InstallersAPI, @@ -770,12 +770,17 @@ def validate_token(self) -> bool: # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info self._get_server_info() - self._token_is_valid = is_token_valid( + user_info = get_user_info_by_token( self.base_url, self._access_token, verify=self._ssl_verify, cert=self._cert ) + self._token_is_valid = user_info.is_valid + is_service = None + if user_info.is_valid: + is_service = user_info.is_service + self._access_token_is_service = is_service except Exception: self._token_is_valid = False self.close_session() @@ -787,7 +792,7 @@ def validate_token(self) -> bool: def set_token(self, token: Optional[str]): self.reset_token() self._access_token = token - self.get_user() + self.validate_token() def reset_token(self): self._access_token = None diff --git a/ayon_api/utils.py b/ayon_api/utils.py index d486db3cf..40505e817 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -3,6 +3,7 @@ import os import re import datetime +from dataclasses import dataclass, field import copy import logging import json @@ -655,19 +656,32 @@ def logout_from_server( ) -def get_user_by_token( +@dataclass +class UserInfo: + """User information.""" + is_valid: bool = False + is_service: bool = False + content: bytes = b"" + data: dict[str, Any] = field(default_factory=dict) + + +def get_user_info_by_token( url: str, token: str, *, verify: str | bool | None = None, cert: str | None = None, timeout: float | None = None, -) -> dict[str, Any] | None: +) -> UserInfo: """Get user information by url and token. Args: url (str): Server url. token (str): User's token. + verify (str | bool | None): SSL verification for request. Value from + 'AYON_CA_FILE' environment variable is used if not specified. + cert (str | None): SSL certificate for request. Value from + 'AYON_CERT_FILE' environment variable is used if not specified. timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. @@ -675,6 +689,10 @@ def get_user_by_token( dict[str, Any] | None: User information if url and token are valid. """ + output = UserInfo() + if not token: + return output + if timeout is None: timeout = get_default_timeout() @@ -687,9 +705,9 @@ def get_user_by_token( base_headers = { "Content-Type": "application/json", } - for header_value in ( - {"Authorization": f"Bearer {token}"}, - {"X-Api-Key": token}, + for header_value, is_service in ( + ({"Authorization": f"Bearer {token}"}, False), + ({"X-Api-Key": token}, True), ): headers = base_headers.copy() headers.update(header_value) @@ -700,18 +718,61 @@ def get_user_by_token( verify=verify, cert=cert, ) - if response.status_code == 200: - return response.json() + try: + data = response.json() + except Exception: + data = {} + + output = UserInfo( + is_valid=response.status_code == 200, + is_service=is_service, + data=data, + content=response.content, + ) + if output.is_valid: + break + return output + + +def get_user_by_token( + url: str, + token: str, + timeout: float | None = None, + *, + verify: str | bool | None = None, + cert: str | None = None, +) -> dict[str, Any] | None: + """Get user information by url and token. + + Args: + url (str): Server url. + token (str): User's token. + timeout (float | None): Timeout for request. Value from + 'get_default_timeout' is used if not specified. + verify (str | bool | None): SSL verification for request. Value from + 'AYON_CA_FILE' environment variable is used if not specified. + cert (str | None): SSL certificate for request. Value from + 'AYON_CERT_FILE' environment variable is used if not specified. + + Returns: + dict[str, Any] | None: User information if url and token are valid. + + """ + user_info = get_user_info_by_token( + url, token, timeout=timeout, verify=verify, cert=cert, + ) + if user_info.is_valid: + return user_info.data return None def is_token_valid( url: str, token: str, + timeout: float | None = None, *, verify: str | bool | None = None, cert: str | None = None, - timeout: float | None = None, ) -> bool: """Check if token is valid. @@ -722,20 +783,19 @@ def is_token_valid( token (str): User's token. timeout (float | None): Timeout for request. Value from 'get_default_timeout' is used if not specified. + verify (str | bool | None): SSL verification for request. Value from + 'AYON_CA_FILE' environment variable is used if not specified. + cert (str | None): SSL certificate for request. Value from + 'AYON_CERT_FILE' environment variable is used if not specified. Returns: bool: True if token is valid. """ - if get_user_by_token( - url, - token, - verify=verify, - cert=cert, - timeout=timeout - ): - return True - return False + user_info = get_user_info_by_token( + url, token, timeout=timeout, verify=verify, cert=cert + ) + return user_info.is_valid def validate_url( From d36a0e9f21ab05286e2a99640010df2ef68957a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:14:12 +0200 Subject: [PATCH 461/506] wrap token information to dataclass and added better handling of it --- ayon_api/_api.py | 6 +- ayon_api/server_api.py | 170 ++++++++++++++++++++++------------------- 2 files changed, 96 insertions(+), 80 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 5c843360f..541c22948 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -129,10 +129,10 @@ def login(self, username: str, password: str): login is skipped. """ - previous_token = self._access_token + previous_token = self._token_info.token super().login(username, password) - if self.has_valid_token and previous_token != self._access_token: - os.environ[SERVER_API_ENV_KEY] = self._access_token + if self.has_valid_token and previous_token != self._token_info.token: + os.environ[SERVER_API_ENV_KEY] = self._token_info.token @staticmethod def get_url(): diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7a6131566..0a935dfb7 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -6,6 +6,7 @@ from __future__ import annotations import copy +from dataclasses import dataclass import os import re import io @@ -214,6 +215,13 @@ def _cleanup(): _cleanup() +@dataclass +class TokenInfo: + token: str | None = None + is_valid: bool | None = None + is_service: bool | None = None + + class ServerAPI( InstallersAPI, DependencyPackagesAPI, @@ -295,7 +303,7 @@ def __init__( self._rest_url: str = f"{base_url}/api" self._graphql_url: str = f"{base_url}/graphql" self._log: logging.Logger = logging.getLogger(self.__class__.__name__) - self._access_token: Optional[str] = token + # Allow to have 'site_id' to 'None' if site_id is NOT_SET: site_id = get_default_site_id() @@ -327,9 +335,8 @@ def __init__( self._ssl_verify = ssl_verify self._cert = cert - self._access_token_is_service = None - self._token_is_valid = None - self._token_validation_started = False + self._token_info = TokenInfo(token=token) + self._server_available = None self._server_version = None self._server_version_tuple = None @@ -356,7 +363,7 @@ def __init__( self._as_user_stack = _AsUserStack() # Create session - if self._access_token and create_session: + if self._token_info.token and create_session: self.validate_server_availability() self.create_session() @@ -503,7 +510,7 @@ def access_token(self) -> Optional[str]: Optional[str]: Token string or None if not authorized yet. """ - return self._access_token + return self.self._token_info.token def is_service_user(self) -> bool: """Check if connection is using service API key. @@ -514,7 +521,7 @@ def is_service_user(self) -> bool: """ if not self.has_valid_token: raise ValueError("User is not logged in.") - return bool(self._access_token_is_service) + return bool(self._token_info.is_service) def get_site_id(self) -> Optional[str]: """Site id used for connection. @@ -683,7 +690,7 @@ def set_default_service_username(self, username: Optional[str] = None): "Authentication of connection did not happen yet." ) - if not self._access_token_is_service: + if not self._token_info.is_service: raise ValueError( "Can't set service username. API key is not a service token." ) @@ -717,7 +724,7 @@ def as_username( "Authentication of connection did not happen yet." ) - if not self._access_token_is_service: + if not self._token_info.is_service: if ignore_service_error: yield None return @@ -745,12 +752,12 @@ def is_server_available(self) -> bool: @property def has_valid_token(self) -> bool: - if self._access_token is None: + if self._token_info.token is None: return False - if self._token_is_valid is None: + if self._token_info.is_valid is None: self.validate_token() - return self._token_is_valid + return self._token_info.is_valid def validate_server_availability(self): if not self.is_server_available: @@ -759,44 +766,44 @@ def validate_server_availability(self): ) def validate_token(self) -> bool: - if self._access_token is None: - self._token_is_valid = False + if self._token_info.token is None: + self._token_info.is_valid = False self.close_session() return False - self._token_validation_started = True try: # TODO add other possible validations # - existence of 'user' key in info # - validate that 'site_id' is in 'sites' in info self._get_server_info() + user_info = get_user_info_by_token( self.base_url, - self._access_token, + self._token_info.token, verify=self._ssl_verify, cert=self._cert ) - self._token_is_valid = user_info.is_valid + self._token_info.is_valid = user_info.is_valid is_service = None if user_info.is_valid: is_service = user_info.is_service - self._access_token_is_service = is_service + self._token_info.is_service = is_service + except Exception: - self._token_is_valid = False + self._token_info.is_valid = False self.close_session() self.log.error("Failed to validate token.", exc_info=True) - finally: - self._token_validation_started = False - return self._token_is_valid + + return self._token_info.is_valid def set_token(self, token: Optional[str]): self.reset_token() - self._access_token = token + self._token_info.token = token self.validate_token() def reset_token(self): - self._access_token = None - self._token_is_valid = None + self._token_info.token = None + self._token_info.is_valid = None self.close_session() def create_session( @@ -1128,14 +1135,14 @@ def get_headers( if self._sender is not None: headers["x-sender"] = self._sender - if self._access_token: - if self._access_token_is_service: - headers["X-Api-Key"] = self._access_token + if self._token_info.token: + if self._token_info.is_service: + headers["X-Api-Key"] = self._token_info.token username = self._as_user_stack.username if username: headers["X-as-user"] = username else: - headers["Authorization"] = f"Bearer {self._access_token}" + headers["Authorization"] = f"Bearer {self._token_info.token}" return headers def login( @@ -1146,7 +1153,7 @@ def login( Args: username (str): Username. password (str): Password. - create_session (Optional[bool]): Create session after login. + create_session (bool): Create session after login. Default: True. Raises: @@ -1170,26 +1177,25 @@ def login( self.validate_server_availability() - self._token_validation_started = True - - try: - response = self.post( - "auth/login", - name=username, - password=password - ) - if response.status_code != 200: - _detail = response.data.get("detail") - details = "" - if _detail: - details = f" {_detail}" - - raise AuthenticationError(f"Login failed {details}") + response = self.post( + "auth/login", + name=username, + password=password, + handle_invalid_token=False, + ) + if response.status_code != 200: + _detail = response.data.get("detail") + details = "" + if _detail: + details = f" {_detail}" - finally: - self._token_validation_started = False + raise AuthenticationError(f"Login failed {details}") - self._access_token = response["token"] + self._token_info.token = response["token"] + # Should be valid if was just loged in + self._token_info.is_valid = True + # Service token can't be obtained by login, so it is not service token + self._token_info.is_service = False if not self.has_valid_token: raise AuthenticationError("Invalid credentials") @@ -1198,7 +1204,7 @@ def login( self.create_session() def logout(self, soft: bool = False): - if self._access_token: + if self._token_info.token: if not soft: self._logout() self.reset_token() @@ -1458,7 +1464,7 @@ def _endpoint_to_url( return f"{base_url}/{endpoint}" def _logout(self): - logout_from_server(self._base_url, self._access_token) + logout_from_server(self._base_url, self._token_info.token) def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" @@ -1471,46 +1477,57 @@ def _get_server_info(self) -> dict[str, Any]: return response.json() def _get_user_info(self) -> Optional[dict[str, Any]]: - if self._access_token is None: + if self._token_info.token is None: return None - if self._access_token_is_service is not None: - response = self.get("users/me") - if response.status == 200: - return response.data - return None + if self._token_info.is_service is None: + if self._token_info.is_valid is False: + return None - self._access_token_is_service = False - response = self.get("users/me") - if response.status == 200: - return response.data + self.validate_token() + + if self._token_info.is_valid is False: + return None - self._access_token_is_service = True response = self.get("users/me") if response.status == 200: return response.data - - self._access_token_is_service = None return None - def _do_rest_request(self, function, url, **kwargs): + def _do_rest_request( + self, + function: Any, + url: str, + *, + handle_invalid_token: bool = True, + **kwargs + ): kwargs.setdefault("timeout", self.timeout) max_retries = kwargs.get("max_retries", self.max_retries) if max_retries < 1: max_retries = 1 - if self._token_is_valid is False: - raise UnauthorizedError( - "Authentication token was invalidated." + if handle_invalid_token and self._token_info.is_valid is False: + # Return a fake error response if the token is known to be invalid. + # Added to prevent DDOS attack on server when many requests + # with invalid token are send. It is better to return error + # immediately without trying to send a request to server. + # NOTE maybe store last know response data and re-use it? + detail = "Access token is missing" + if self._token_info.is_service: + detail = "Invalid API key" + new_response = RestApiResponse( + None, + {"code": 401, "detail": detail} ) + new_response.status = 401 + return new_response if self._session is None: # Validate token if was not yet validated - # - ignore validation if we're in middle of - # validation if ( - self._token_is_valid is None - and not self._token_validation_started + handle_invalid_token + and self._token_info.is_valid is None ): self.validate_token() @@ -1589,16 +1606,15 @@ def _do_rest_request(self, function, url, **kwargs): if new_response is not None: return new_response + new_response = RestApiResponse(response) if ( - response is not None - and self._token_is_valid - and response.status_code == 401 + handle_invalid_token + and new_response.status_code == 401 + and self._token_info.is_valid ): - self._token_is_valid = False + self._token_info.is_valid = False self.close_session() - self._trigger_on_invalidate_callbacks() - new_response = RestApiResponse(response) self.log.debug(f"Response {str(new_response)}") return new_response From bba0f17cc09d933a66989bd5e4cd7cea576a6994 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:39:03 +0200 Subject: [PATCH 462/506] few overall fixes --- ayon_api/server_api.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 0a935dfb7..d7e93eaf2 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -887,7 +887,7 @@ def get_info(self) -> dict[str, Any]: dict[str, Any]: Information from server. """ - if self._session is None: + if self._token_info.is_valid is None: return self._get_server_info() response = self.get("info") @@ -1464,7 +1464,8 @@ def _endpoint_to_url( return f"{base_url}/{endpoint}" def _logout(self): - logout_from_server(self._base_url, self._token_info.token) + if self._token_info.is_valid: + logout_from_server(self._base_url, self._token_info.token) def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" @@ -1477,18 +1478,17 @@ def _get_server_info(self) -> dict[str, Any]: return response.json() def _get_user_info(self) -> Optional[dict[str, Any]]: - if self._token_info.token is None: + if ( + self._token_info.token is None + or self._token_info.is_valid is False + ): return None if self._token_info.is_service is None: + self.validate_token() if self._token_info.is_valid is False: return None - self.validate_token() - - if self._token_info.is_valid is False: - return None - response = self.get("users/me") if response.status == 200: return response.data From 1b01a9d7fbdc43c198b477a21fbbcefc694ac049 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 8 Jun 2026 17:57:56 +0200 Subject: [PATCH 463/506] mark few functions for deprecation --- ayon_api/utils.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 40505e817..35ef5262c 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -1,5 +1,6 @@ from __future__ import annotations +import functools import os import re import datetime @@ -16,6 +17,7 @@ from urllib.parse import urlparse, urlencode, ParseResult import typing from typing import Any, Iterable +import warnings from enum import IntEnum import requests @@ -68,6 +70,38 @@ ) ) +@dataclass +class _TimeoutWrapInfo: + func = None + args_pos = 2 + + +def _timeout_kwarg_deprecation(arg): + """Decorator to add timeout kwarg to function.""" + # TODO remove this deprecation + wrap_info = _TimeoutWrapInfo() + + def wrapper(*args, **kwargs): + if len(args) > wrap_info.args_pos: + warnings.warn( + "Timeout was passed as a positional argument please" + " use timeout=... keyword argument instead. This will stop" + " working in future versions on ayon-api.", + category=FutureWarning, + stacklevel=2, + ) + return wrap_info.func(*args, **kwargs) + + if not isinstance(arg, int): + wrap_info.func = arg + return functools.wraps(arg)(wrapper) + + wrap_info.args_pos = arg + def main_wrapper(func): + wrap_info.func = func + return functools.wraps(func)(wrapper) + return main_wrapper + class SortOrder(IntEnum): """Sort order for GraphQl requests.""" @@ -588,6 +622,7 @@ def _try_connect_to_server( return None +@_timeout_kwarg_deprecation(3) def login_to_server( url: str, username: str, @@ -629,6 +664,7 @@ def login_to_server( return token +@_timeout_kwarg_deprecation def logout_from_server( url: str, token: str, @@ -734,6 +770,7 @@ def get_user_info_by_token( return output +@_timeout_kwarg_deprecation def get_user_by_token( url: str, token: str, @@ -766,6 +803,7 @@ def get_user_by_token( return None +@_timeout_kwarg_deprecation def is_token_valid( url: str, token: str, @@ -798,6 +836,7 @@ def is_token_valid( return user_info.is_valid +@_timeout_kwarg_deprecation(1) def validate_url( url: str, timeout: int | None = None, From a7ad9da4eccb5ad8011842d58b0aee863e06ff6c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:00:35 +0200 Subject: [PATCH 464/506] pass timeout --- ayon_api/server_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d7e93eaf2..ee6a56baa 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -781,7 +781,8 @@ def validate_token(self) -> bool: self.base_url, self._token_info.token, verify=self._ssl_verify, - cert=self._cert + cert=self._cert, + timeout=self.timeout, ) self._token_info.is_valid = user_info.is_valid is_service = None @@ -1473,6 +1474,7 @@ def _get_server_info(self) -> dict[str, Any]: f"{self._rest_url}/info", cert=self._cert, verify=self._ssl_verify, + timeout=self.timeout, ) response.raise_for_status() return response.json() From 2cb728d6bd211c7b157f95d721d44d76ccc48be5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:00:54 +0200 Subject: [PATCH 465/506] fix typo --- 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 ee6a56baa..7c5c80739 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1193,7 +1193,7 @@ def login( raise AuthenticationError(f"Login failed {details}") self._token_info.token = response["token"] - # Should be valid if was just loged in + # Should be valid if was just logged in self._token_info.is_valid = True # Service token can't be obtained by login, so it is not service token self._token_info.is_service = False From 9eb311e54359678ba22abfc2dcfd396e4111aef7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:22:33 +0200 Subject: [PATCH 466/506] store the unauthorized response --- ayon_api/server_api.py | 12 ++++++++---- ayon_api/utils.py | 10 ++-------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7c5c80739..a3641aa5b 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -220,6 +220,7 @@ class TokenInfo: token: str | None = None is_valid: bool | None = None is_service: bool | None = None + unauthorized_response: requests.Response | None = None class ServerAPI( @@ -768,6 +769,7 @@ def validate_server_availability(self): def validate_token(self) -> bool: if self._token_info.token is None: self._token_info.is_valid = False + self._token_info.unauthorized_response = None self.close_session() return False @@ -785,6 +787,7 @@ def validate_token(self) -> bool: timeout=self.timeout, ) self._token_info.is_valid = user_info.is_valid + self._token_info.unauthorized_response = user_info.response is_service = None if user_info.is_valid: is_service = user_info.is_service @@ -792,6 +795,7 @@ def validate_token(self) -> bool: except Exception: self._token_info.is_valid = False + self._token_info.unauthorized_response = None self.close_session() self.log.error("Failed to validate token.", exc_info=True) @@ -1514,10 +1518,9 @@ def _do_rest_request( # Added to prevent DDOS attack on server when many requests # with invalid token are send. It is better to return error # immediately without trying to send a request to server. - # NOTE maybe store last know response data and re-use it? - detail = "Access token is missing" - if self._token_info.is_service: - detail = "Invalid API key" + if self._token_info.unauthorized_response is not None: + return RestApiResponse(self._token_info.unauthorized_response) + new_response = RestApiResponse( None, {"code": 401, "detail": detail} @@ -1615,6 +1618,7 @@ def _do_rest_request( and self._token_info.is_valid ): self._token_info.is_valid = False + self._token_info.unauthorized_response = response self.close_session() self.log.debug(f"Response {str(new_response)}") diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 35ef5262c..6e5a4b388 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -697,8 +697,7 @@ class UserInfo: """User information.""" is_valid: bool = False is_service: bool = False - content: bytes = b"" - data: dict[str, Any] = field(default_factory=dict) + response: requests.Response | None = None def get_user_info_by_token( @@ -754,16 +753,11 @@ def get_user_info_by_token( verify=verify, cert=cert, ) - try: - data = response.json() - except Exception: - data = {} output = UserInfo( is_valid=response.status_code == 200, is_service=is_service, - data=data, - content=response.content, + response=response, ) if output.is_valid: break From 2a3ce1658e94e3e35f3925b876f4bf3f5dc1d9a7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:24:09 +0200 Subject: [PATCH 467/506] mark ayon api errors with prefix --- ayon_api/server_api.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index a3641aa5b..205de2c23 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1523,7 +1523,7 @@ def _do_rest_request( new_response = RestApiResponse( None, - {"code": 401, "detail": detail} + {"code": 401, "detail": "AYON api error: Invalid API key"} ) new_response.status = 401 return new_response @@ -1569,7 +1569,8 @@ def _do_rest_request( except ConnectionRefusedError: if retry_idx == 0: self.log.warning( - "Connection error happened.", exc_info=True + "AYON api error: Connection error happened.", + exc_info=True, ) # Server may be restarting @@ -1577,7 +1578,8 @@ def _do_rest_request( None, { "detail": ( - "Unable to connect the server. Connection refused" + "AYON api error: Unable to connect the server." + " Connection refused" ) } ) @@ -1586,21 +1588,23 @@ def _do_rest_request( # Connection timed out new_response = RestApiResponse( None, - {"detail": "Connection timed out."} + {"detail": "AYON api error: Connection timed out."} ) except requests.exceptions.ConnectionError: # Log warning only on last attempt if retry_idx == 0: self.log.warning( - "Connection error happened.", exc_info=True + "AYON api error: Connection error happened.", + exc_info=True ) new_response = RestApiResponse( None, { "detail": ( - "Unable to connect the server. Connection error" + "AYON api error: Unable to connect the server." + " Connection error." ) } ) From 09a58ce5de339e55f3ff9548440b388fa7e33dc3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:24:45 +0200 Subject: [PATCH 468/506] reset is_service too --- ayon_api/server_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 205de2c23..92540ec15 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -808,6 +808,7 @@ def set_token(self, token: Optional[str]): def reset_token(self): self._token_info.token = None + self._token_info.is_service = None self._token_info.is_valid = None self.close_session() From 347293d011b233aa8342b4104dbb1ebd0421575f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:25:04 +0200 Subject: [PATCH 469/506] change how errors are handled in rest api response --- ayon_api/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index 6e5a4b388..b00185964 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -207,11 +207,11 @@ def ok(self) -> bool: def raise_for_status(self, message=None): if self._response is None: if self._data and self._data.get("detail"): + if self.status_code == 401: + raise UnauthorizedError(self._data["detail"]) raise ServerError(self._data["detail"]) raise ValueError("Response is not available.") - if self.status_code == 401: - raise UnauthorizedError("Missing or invalid authentication token") try: self._response.raise_for_status() except requests.exceptions.HTTPError as exc: @@ -232,6 +232,8 @@ def raise_for_status(self, message=None): detail = self.data.get("detail") if detail: message = f"{message} ({detail})" + if self.status_code == 401: + raise UnauthorizedError(message, exc.response) raise HTTPRequestError(message, exc.response) def __enter__(self, *args, **kwargs): From 7358aedbfc669b47ff8a587f04d3984927e263ec Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:33:32 +0200 Subject: [PATCH 470/506] use raw_post when using 'handle_invalid_token' --- ayon_api/server_api.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 92540ec15..220ef7cc0 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1183,10 +1183,12 @@ def login( self.validate_server_availability() - response = self.post( + response = self.raw_post( "auth/login", - name=username, - password=password, + json=dict( + name=username, + password=password, + ), handle_invalid_token=False, ) if response.status_code != 200: From 78f38427371058eed2a484f7af636f1adb57b52b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:39:21 +0200 Subject: [PATCH 471/506] fix doubled self --- 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 220ef7cc0..d09601e80 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -511,7 +511,7 @@ def access_token(self) -> Optional[str]: Optional[str]: Token string or None if not authorized yet. """ - return self.self._token_info.token + return self._token_info.token def is_service_user(self) -> bool: """Check if connection is using service API key. From 0485ad6f347a7a787020d45a2dfbc783550c6572 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:41:45 +0200 Subject: [PATCH 472/506] pass timeout to requests request --- ayon_api/server_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index d09601e80..cfefc3289 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -746,7 +746,8 @@ def is_server_available(self) -> bool: response = requests.get( self._base_url, cert=self._cert, - verify=self._ssl_verify + verify=self._ssl_verify, + timeout=self.timeout, ) self._server_available = response.status_code == 200 return self._server_available From 7d2c512ec570d82269c16fe91f18f6d3a40510d4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:41:57 +0200 Subject: [PATCH 473/506] fix return type in 'get_user_info_by_token' --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index b00185964..fb7f8cc32 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -723,7 +723,7 @@ def get_user_info_by_token( 'get_default_timeout' is used if not specified. Returns: - dict[str, Any] | None: User information if url and token are valid. + UserInfo: User information if url and token are valid. """ output = UserInfo() From 865861ac5d6ef6aee6b546a5088a5dabf0f87a05 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:46:58 +0200 Subject: [PATCH 474/506] unset more attributes --- ayon_api/server_api.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index cfefc3289..60a5d11dc 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -796,6 +796,7 @@ def validate_token(self) -> bool: except Exception: self._token_info.is_valid = False + self._token_info.is_service = None self._token_info.unauthorized_response = None self.close_session() self.log.error("Failed to validate token.", exc_info=True) @@ -811,6 +812,7 @@ def reset_token(self): self._token_info.token = None self._token_info.is_service = None self._token_info.is_valid = None + self._token_info.unauthorized_response = None self.close_session() def create_session( From 196e7cbba5a8b5032371736afc3307cf2a136395 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:49:20 +0200 Subject: [PATCH 475/506] remove unsed import --- ayon_api/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/utils.py b/ayon_api/utils.py index fb7f8cc32..ab2e4c7dd 100644 --- a/ayon_api/utils.py +++ b/ayon_api/utils.py @@ -4,7 +4,7 @@ import os import re import datetime -from dataclasses import dataclass, field +from dataclasses import dataclass import copy import logging import json From a1798328ff1efffed7856d16d76795131b38e494 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 10:54:36 +0200 Subject: [PATCH 476/506] do not add token to headers if is not valid --- 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 60a5d11dc..e442c6f6a 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1144,7 +1144,7 @@ def get_headers( if self._sender is not None: headers["x-sender"] = self._sender - if self._token_info.token: + if self._token_info.token and self._token_info.is_valid is not False: if self._token_info.is_service: headers["X-Api-Key"] = self._token_info.token username = self._as_user_stack.username From 79cb24296b49d0fb84608a1fc25cd2beeae6cb27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 11:11:11 +0200 Subject: [PATCH 477/506] don't use raw requests for server info --- ayon_api/server_api.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index e442c6f6a..7347ca231 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -896,10 +896,15 @@ def get_info(self) -> dict[str, Any]: dict[str, Any]: Information from server. """ - if self._token_info.is_valid is None: - return self._get_server_info() + handle_invalid_token = ( + self._token_info.token + and self._token_info.is_valid + ) - response = self.get("info") + response = self.raw_get( + "info", + handle_invalid_token=handle_invalid_token, + ) response.raise_for_status() return response.data @@ -1480,14 +1485,12 @@ def _logout(self): def _get_server_info(self) -> dict[str, Any]: """Get server info without a session.""" - response = requests.get( - f"{self._rest_url}/info", - cert=self._cert, - verify=self._ssl_verify, - timeout=self.timeout, + response = self.raw_get( + "info", + handle_invalid_token=False, ) response.raise_for_status() - return response.json() + return response.data def _get_user_info(self) -> Optional[dict[str, Any]]: if ( From 7f81a75d88567509d647e5b4c5ef8f5009b71559 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Tue, 9 Jun 2026 18:12:21 +0200 Subject: [PATCH 478/506] updated lists api --- ayon_api/_api_helpers/lists.py | 214 ++++++++++++++++++++++++++++++++- 1 file changed, 213 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 17827266d..a009fab5d 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -4,11 +4,12 @@ import typing from typing import Optional, Iterable, Any, Generator -from ayon_api.utils import create_entity_id +from ayon_api.utils import NOT_SET, create_entity_id from ayon_api.graphql_queries import entity_lists_graphql_query from .base import BaseServerAPI + if typing.TYPE_CHECKING: from ayon_api.typing import ( EntityListEntityType, @@ -159,6 +160,7 @@ def create_entity_list( data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, template: Optional[dict[str, Any]] = None, + entity_list_folder_id: Optional[str] = None, owner: Optional[str] = None, active: Optional[bool] = None, items: Optional[list[dict[str, Any]]] = None, @@ -178,6 +180,8 @@ def create_entity_list( data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. template (Optional[dict[str, Any]]): Dynamic list template. + entity_list_folder_id (Optional[dict[str, Any]]): Entity list + folder id. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. items (Optional[list[dict[str, Any]]]): Initial items in @@ -199,6 +203,7 @@ def create_entity_list( ("template", template), ("tags", tags), ("owner", owner), + ("entityListFolderId", entity_list_folder_id), ("data", data), ("active", active), ("items", items), @@ -223,6 +228,7 @@ def update_entity_list( attrib: Optional[list[dict[str, Any]]] = None, data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, + entity_list_folder_id: str | None | NOT_SET = NOT_SET, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -237,6 +243,9 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. + entity_list_folder_id (dict[str, Any] | None | NOT_SET): New + entity list folder id. Use 'None' to move entity list to root. + Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. @@ -254,6 +263,9 @@ def update_entity_list( ) if value is not None } + if entity_list_folder_id is not NOT_SET: + kwargs["entityListFolderId"] = entity_list_folder_id + response = self.patch( f"projects/{project_name}/lists/{list_id}", **kwargs @@ -454,3 +466,203 @@ def delete_entity_list_item( f"projects/{project_name}/lists/{list_id}/items/{item_id}", ) response.raise_for_status() + + def get_entity_list_entities( + self, project_name: str, entity_list_id: str + ) -> dict[str, Any]: + """Get entity list items using REST API. + + Args: + project_name (str): Project name. + entity_list_id (str): Entity list id. + + Returns: + dict[str, Any]: Information about entities on the list. + + """ + response = self.get( + f"projects/{project_name}/lists/{entity_list_id}/entities" + ) + response.raise_for_status() + return response.data + + def get_entity_list_folders_raw(self, project_name: str) -> dict: + """Get entity list folders. + + Returns: + dict[str, Any]: Raw output of entity list folders output. At this + moment contains only "folders" key with list of folders, + but it can be extended in the future. + + """ + response = self.get(f"projects/{project_name}/entityListFolders") + response.raise_for_status() + return response.data + + def get_entity_list_folders( + self, project_name: str + ) -> list[dict[str, Any]]: + """Get entity list folders. + + Returns: + list[dict[str, Any]]: List of entity list folders. + + """ + data = self.get_entity_list_folders_raw(project_name) + return data["folders"] + + def create_entity_list_folder( + self, + project_name: str, + label: str, + *, + parent_id: str | None = None, + color: str | None = None, + icon: str | None = None, + scope: list[str] | None = None, + data: dict | None = None, + access: dict | None = None, + entity_list_folder_id: str | None = None, + ) -> str: + """Create entity list folder. + + Args: + project_name (str): Project name. + label (str): Folder label. + parent_id (str | None): Parent folder id. If None, the folder will + be created in root. + color (str | None): Folder color. + icon (str | None): Folder icon. + scope (list[str] | None): Folder scope. + data (dict | None): Custom data of entity list folder. + access (dict | None): Access control for entity list folder. + entity_list_folder_id (str | None): Id of folder that will be + created. If None, a new id will be generated. + + Returns: + str: Created entity list folder id. + + """ + if data is None: + data = {} + + for key, value in ( + ("color", color), + ("icon", icon), + ("scope", scope), + ): + if value: + data[key] = value + + if not entity_list_folder_id: + entity_list_folder_id = create_entity_id() + body = { + "id": entity_list_folder_id, + "label": label, + } + if parent_id: + body["parentId"] = parent_id + + if data: + body["data"] = data + + if access: + body["access"] = access + + response = self.post( + f"projects/{project_name}/entityListFolders", + **body + ) + response.raise_for_status() + return entity_list_folder_id + + def update_entity_list_folder( + self, + project_name: str, + entity_list_folder_id: str, + *, + label: str | None = None, + parent_id: str | None| NOT_SET = NOT_SET, + color: str | None = None, + icon: str | None = None, + scope: list[str] | None = None, + data: dict | None = None, + access: dict | None = None, + ) -> None: + """Update entity list folder. + + Args: + project_name (str): Project name. + entity_list_folder_id (str): Folder id that will be updated. + label (str | None): New label of entity list folder. + parent_id (str | None | NOT_SET): New parent id of entity list + folder. If None, the folder will be moved to root. + color (str | None): New color of entity list folder. + icon (str | None): New icon of entity list folder. + scope (list[str] | None): New scope of entity list folder. + data (dict | None): Custom data of entity list folder. + access (dict | None): Access control for entity list folder. + + """ + if data is None: + data = {} + + for key, value in ( + ("color", color), + ("icon", icon), + ): + if value: + data[key] = value + + if scope is not None: + data["scope"] = scope + + body = {} + if data: + body["data"] = data + if label: + body["label"] = label + if access is not None: + body["access"] = access + if parent_id is not NOT_SET: + body["parentId"] = parent_id + + if not body: + return + + response = self.patch( + ( + f"projects/{project_name}/" + f"entityListFolders/{entity_list_folder_id}" + ), + **body + ) + response.raise_for_status() + + def delete_entity_list_folder( + self, + project_name: str, + entity_list_folder_id: str, + ) -> None: + """Delete entity list folder.""" + response = self.delete( + f"projects/{project_name}/" + f"entityListFolders/{entity_list_folder_id}" + ) + response.raise_for_status() + + def set_entity_list_folders_order( + self, project_name: str, order: list[str] + ) -> None: + """Change order of entity list folders. + + Args: + project_name (str): Project name. + order (list[str]): List of folder ids in desired order. + + """ + response = self.post( + f"projects/{project_name}/entityListFolders/order", + order=order, + ) + response.raise_for_status() From 633626aa7fcf53b48065bbdf81089f5013a376d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:06:05 +0200 Subject: [PATCH 479/506] print wrong typehint in automated api --- automated_api.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/automated_api.py b/automated_api.py index 766cd4bc3..f7c1d01ea 100644 --- a/automated_api.py +++ b/automated_api.py @@ -199,6 +199,9 @@ def _get_typehint(annotation, api_globals): return typehint except NameError: print("Unknown typehint:", typehint) + except Exception: + print("Error while processing typehint:", typehint) + raise _typehint = typehint _typehing_parents = [] From 693ba6e244b3b0cca5d1c1b4d07b8f782c7a05cf Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:06:22 +0200 Subject: [PATCH 480/506] fix typehints using NOT_SET --- ayon_api/_api_helpers/lists.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index a009fab5d..7baf049eb 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -228,7 +228,7 @@ def update_entity_list( attrib: Optional[list[dict[str, Any]]] = None, data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, - entity_list_folder_id: str | None | NOT_SET = NOT_SET, + entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -243,7 +243,7 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. - entity_list_folder_id (dict[str, Any] | None | NOT_SET): New + entity_list_folder_id (dict[str, Any] | None | type[NOT_SET]): New entity list folder id. Use 'None' to move entity list to root. Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. @@ -582,7 +582,7 @@ def update_entity_list_folder( entity_list_folder_id: str, *, label: str | None = None, - parent_id: str | None| NOT_SET = NOT_SET, + parent_id: str | None| type[NOT_SET] = NOT_SET, color: str | None = None, icon: str | None = None, scope: list[str] | None = None, @@ -595,8 +595,8 @@ def update_entity_list_folder( project_name (str): Project name. entity_list_folder_id (str): Folder id that will be updated. label (str | None): New label of entity list folder. - parent_id (str | None | NOT_SET): New parent id of entity list - folder. If None, the folder will be moved to root. + parent_id (str | None | type[NOT_SET]): New parent id of entity + list folder. If None, the folder will be moved to root. color (str | None): New color of entity list folder. icon (str | None): New icon of entity list folder. scope (list[str] | None): New scope of entity list folder. From 57302c11d065fb29c58d20e4d3073896baf7bf0d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:06:36 +0200 Subject: [PATCH 481/506] update public api --- ayon_api/__init__.py | 14 ++++ ayon_api/_api.py | 179 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index a125e8993..3d5f89311 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -298,6 +298,13 @@ update_entity_list_items, update_entity_list_item, delete_entity_list_item, + get_entity_list_entities, + get_entity_list_folders_raw, + get_entity_list_folders, + create_entity_list_folder, + update_entity_list_folder, + delete_entity_list_folder, + set_entity_list_folders_order, get_thumbnail_by_id, get_thumbnail, get_folder_thumbnail, @@ -609,6 +616,13 @@ "update_entity_list_items", "update_entity_list_item", "delete_entity_list_item", + "get_entity_list_entities", + "get_entity_list_folders_raw", + "get_entity_list_folders", + "create_entity_list_folder", + "update_entity_list_folder", + "delete_entity_list_folder", + "set_entity_list_folders_order", "get_thumbnail_by_id", "get_thumbnail", "get_folder_thumbnail", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 5c843360f..1c0bb9861 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -7981,6 +7981,7 @@ def create_entity_list( data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, template: Optional[dict[str, Any]] = None, + entity_list_folder_id: Optional[str] = None, owner: Optional[str] = None, active: Optional[bool] = None, items: Optional[list[dict[str, Any]]] = None, @@ -8000,6 +8001,8 @@ def create_entity_list( data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. template (Optional[dict[str, Any]]): Dynamic list template. + entity_list_folder_id (Optional[dict[str, Any]]): Entity list + folder id. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. items (Optional[list[dict[str, Any]]]): Initial items in @@ -8018,6 +8021,7 @@ def create_entity_list( data=data, tags=tags, template=template, + entity_list_folder_id=entity_list_folder_id, owner=owner, active=active, items=items, @@ -8034,6 +8038,7 @@ def update_entity_list( attrib: Optional[list[dict[str, Any]]] = None, data: Optional[list[dict[str, Any]]] = None, tags: Optional[list[str]] = None, + entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, owner: Optional[str] = None, active: Optional[bool] = None, ) -> None: @@ -8048,6 +8053,9 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. + entity_list_folder_id (dict[str, Any] | None | type[NOT_SET]): New + entity list folder id. Use 'None' to move entity list to root. + Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. @@ -8061,6 +8069,7 @@ def update_entity_list( attrib=attrib, data=data, tags=tags, + entity_list_folder_id=entity_list_folder_id, owner=owner, active=active, ) @@ -8259,6 +8268,176 @@ def delete_entity_list_item( ) +def get_entity_list_entities( + project_name: str, + entity_list_id: str, +) -> dict[str, Any]: + """Get entity list items using REST API. + + Args: + project_name (str): Project name. + entity_list_id (str): Entity list id. + + Returns: + dict[str, Any]: Information about entities on the list. + + """ + con = get_server_api_connection() + return con.get_entity_list_entities( + project_name=project_name, + entity_list_id=entity_list_id, + ) + + +def get_entity_list_folders_raw( + project_name: str, +) -> dict: + """Get entity list folders. + + Returns: + dict[str, Any]: Raw output of entity list folders output. At this + moment contains only "folders" key with list of folders, + but it can be extended in the future. + + """ + con = get_server_api_connection() + return con.get_entity_list_folders_raw( + project_name=project_name, + ) + + +def get_entity_list_folders( + project_name: str, +) -> list[dict[str, Any]]: + """Get entity list folders. + + Returns: + list[dict[str, Any]]: List of entity list folders. + + """ + con = get_server_api_connection() + return con.get_entity_list_folders( + project_name=project_name, + ) + + +def create_entity_list_folder( + project_name: str, + label: str, + *, + parent_id: str | None = None, + color: str | None = None, + icon: str | None = None, + scope: list[str] | None = None, + data: dict | None = None, + access: dict | None = None, + entity_list_folder_id: str | None = None, +) -> str: + """Create entity list folder. + + Args: + project_name (str): Project name. + label (str): Folder label. + parent_id (str | None): Parent folder id. If None, the folder will + be created in root. + color (str | None): Folder color. + icon (str | None): Folder icon. + scope (list[str] | None): Folder scope. + data (dict | None): Custom data of entity list folder. + access (dict | None): Access control for entity list folder. + entity_list_folder_id (str | None): Id of folder that will be + created. If None, a new id will be generated. + + Returns: + str: Created entity list folder id. + + """ + con = get_server_api_connection() + return con.create_entity_list_folder( + project_name=project_name, + label=label, + parent_id=parent_id, + color=color, + icon=icon, + scope=scope, + data=data, + access=access, + entity_list_folder_id=entity_list_folder_id, + ) + + +def update_entity_list_folder( + project_name: str, + entity_list_folder_id: str, + *, + label: str | None = None, + parent_id: str | None | type[NOT_SET] = NOT_SET, + color: str | None = None, + icon: str | None = None, + scope: list[str] | None = None, + data: dict | None = None, + access: dict | None = None, +) -> None: + """Update entity list folder. + + Args: + project_name (str): Project name. + entity_list_folder_id (str): Folder id that will be updated. + label (str | None): New label of entity list folder. + parent_id (str | None | type[NOT_SET]): New parent id of entity + list folder. If None, the folder will be moved to root. + color (str | None): New color of entity list folder. + icon (str | None): New icon of entity list folder. + scope (list[str] | None): New scope of entity list folder. + data (dict | None): Custom data of entity list folder. + access (dict | None): Access control for entity list folder. + + """ + con = get_server_api_connection() + return con.update_entity_list_folder( + project_name=project_name, + entity_list_folder_id=entity_list_folder_id, + label=label, + parent_id=parent_id, + color=color, + icon=icon, + scope=scope, + data=data, + access=access, + ) + + +def delete_entity_list_folder( + project_name: str, + entity_list_folder_id: str, +) -> None: + """Delete entity list folder. + """ + con = get_server_api_connection() + return con.delete_entity_list_folder( + project_name=project_name, + entity_list_folder_id=entity_list_folder_id, + ) + + +def set_entity_list_folders_order( + project_name: str, + order: list[str], +) -> None: + """Change order of entity list folders. + + Args: + project_name (str): Project name. + order (list[str]): List of folder ids in desired order. + + """ + con = get_server_api_connection() + return con.set_entity_list_folders_order( + project_name=project_name, + order=order, + ) + + def get_thumbnail_by_id( project_name: str, thumbnail_id: str, From 49bf525ce695c018a228fcd57098671516c7373c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 11 Jun 2026 16:44:56 +0200 Subject: [PATCH 482/506] remove trailing spaces --- ayon_api/_api_helpers/lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 7baf049eb..e5b4a2b25 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -655,7 +655,7 @@ def set_entity_list_folders_order( self, project_name: str, order: list[str] ) -> None: """Change order of entity list folders. - + Args: project_name (str): Project name. order (list[str]): List of folder ids in desired order. From dce11755ff2e074ce229b98c4c109262fc35abe2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jun 2026 09:53:26 +0200 Subject: [PATCH 483/506] type fixes --- ayon_api/_api.py | 39 +++++++++++++++++------------- ayon_api/_api_helpers/lists.py | 43 ++++++++++++++++++++-------------- ayon_api/typing.py | 5 ++++ 3 files changed, 54 insertions(+), 33 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 1c0bb9861..dba95f59f 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -48,6 +48,7 @@ ActivityReferenceType, EntityListEntityType, EntityListItemMode, + EntityListScope, BackgroundOperationTask, LinkDirection, EventFilter, @@ -8001,8 +8002,7 @@ def create_entity_list( data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. template (Optional[dict[str, Any]]): Dynamic list template. - entity_list_folder_id (Optional[dict[str, Any]]): Entity list - folder id. + entity_list_folder_id (Optional[str]): Entity list folder id. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. items (Optional[list[dict[str, Any]]]): Initial items in @@ -8053,7 +8053,7 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. - entity_list_folder_id (dict[str, Any] | None | type[NOT_SET]): New + entity_list_folder_id (str | None | type[NOT_SET]): New entity list folder id. Use 'None' to move entity list to root. Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. @@ -8291,9 +8291,12 @@ def get_entity_list_entities( def get_entity_list_folders_raw( project_name: str, -) -> dict: +) -> dict[str, Any]: """Get entity list folders. + Args: + project_name (str): Project name. + Returns: dict[str, Any]: Raw output of entity list folders output. At this moment contains only "folders" key with list of folders, @@ -8328,9 +8331,9 @@ def create_entity_list_folder( parent_id: str | None = None, color: str | None = None, icon: str | None = None, - scope: list[str] | None = None, - data: dict | None = None, - access: dict | None = None, + scope: list[EntityListScope] | Non] = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, entity_list_folder_id: str | None = None, ) -> str: """Create entity list folder. @@ -8342,9 +8345,11 @@ def create_entity_list_folder( be created in root. color (str | None): Folder color. icon (str | None): Folder icon. - scope (list[str] | None): Folder scope. - data (dict | None): Custom data of entity list folder. - access (dict | None): Access control for entity list folder. + scope (list[EntityListScope] | None): Folder scope. Empty list can + be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. entity_list_folder_id (str | None): Id of folder that will be created. If None, a new id will be generated. @@ -8374,9 +8379,9 @@ def update_entity_list_folder( parent_id: str | None | type[NOT_SET] = NOT_SET, color: str | None = None, icon: str | None = None, - scope: list[str] | None = None, - data: dict | None = None, - access: dict | None = None, + scope: list[EntityListScope] | Non] = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, ) -> None: """Update entity list folder. @@ -8388,9 +8393,11 @@ def update_entity_list_folder( list folder. If None, the folder will be moved to root. color (str | None): New color of entity list folder. icon (str | None): New icon of entity list folder. - scope (list[str] | None): New scope of entity list folder. - data (dict | None): Custom data of entity list folder. - access (dict | None): Access control for entity list folder. + scope (list[EntityListScope] | None): New scope of entity list + folder. Empty list can be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. """ con = get_server_api_connection() diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index e5b4a2b25..76fa14572 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -15,6 +15,7 @@ EntityListEntityType, EntityListAttributeDefinitionDict, EntityListItemMode, + EntityListScope, ) @@ -180,8 +181,7 @@ def create_entity_list( data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. template (Optional[dict[str, Any]]): Dynamic list template. - entity_list_folder_id (Optional[dict[str, Any]]): Entity list - folder id. + entity_list_folder_id (Optional[str]): Entity list folder id. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. items (Optional[list[dict[str, Any]]]): Initial items in @@ -243,7 +243,7 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. - entity_list_folder_id (dict[str, Any] | None | type[NOT_SET]): New + entity_list_folder_id (str | None | type[NOT_SET]): New entity list folder id. Use 'None' to move entity list to root. Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. @@ -486,9 +486,14 @@ def get_entity_list_entities( response.raise_for_status() return response.data - def get_entity_list_folders_raw(self, project_name: str) -> dict: + def get_entity_list_folders_raw( + self, project_name: str + ) -> dict[str, Any]: """Get entity list folders. + Args: + project_name (str): Project name. + Returns: dict[str, Any]: Raw output of entity list folders output. At this moment contains only "folders" key with list of folders, @@ -519,9 +524,9 @@ def create_entity_list_folder( parent_id: str | None = None, color: str | None = None, icon: str | None = None, - scope: list[str] | None = None, - data: dict | None = None, - access: dict | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, entity_list_folder_id: str | None = None, ) -> str: """Create entity list folder. @@ -533,9 +538,11 @@ def create_entity_list_folder( be created in root. color (str | None): Folder color. icon (str | None): Folder icon. - scope (list[str] | None): Folder scope. - data (dict | None): Custom data of entity list folder. - access (dict | None): Access control for entity list folder. + scope (list[EntityListScope] | None): Folder scope. Empty list can + be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. entity_list_folder_id (str | None): Id of folder that will be created. If None, a new id will be generated. @@ -582,12 +589,12 @@ def update_entity_list_folder( entity_list_folder_id: str, *, label: str | None = None, - parent_id: str | None| type[NOT_SET] = NOT_SET, + parent_id: str | None | type[NOT_SET] = NOT_SET, color: str | None = None, icon: str | None = None, - scope: list[str] | None = None, - data: dict | None = None, - access: dict | None = None, + scope: list[EntityListScope] | None = None, + data: dict[str, Any] | None = None, + access: dict[str, Any] | None = None, ) -> None: """Update entity list folder. @@ -599,9 +606,11 @@ def update_entity_list_folder( list folder. If None, the folder will be moved to root. color (str | None): New color of entity list folder. icon (str | None): New icon of entity list folder. - scope (list[str] | None): New scope of entity list folder. - data (dict | None): Custom data of entity list folder. - access (dict | None): Access control for entity list folder. + scope (list[EntityListScope] | None): New scope of entity list + folder. Empty list can be used to scope folder for all views. + data (dict[str, Any] | None): Custom data of entity list folder. + access (dict[str, Any] | None): Access control for + entity list folder. """ if data is None: diff --git a/ayon_api/typing.py b/ayon_api/typing.py index f8016524f..dd1df5085 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -47,6 +47,11 @@ "delete", ] +EntityListScope = Literal[ + "generic", + "review-session", +] + EventFilterValueType = Union[ None, str, int, float, From 00eb34ee9536a73162b45af7ae1d147058f89aff Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:11:16 +0200 Subject: [PATCH 484/506] better automated api --- automated_api.py | 46 ++++++++++------------------------------------ 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/automated_api.py b/automated_api.py index f7c1d01ea..ab6ced2d9 100644 --- a/automated_api.py +++ b/automated_api.py @@ -197,44 +197,10 @@ def _get_typehint(annotation, api_globals): # Test if typehint is valid for known '_api' content exec(f"_: {typehint} = None", api_globals) return typehint - except NameError: - print("Unknown typehint:", typehint) except Exception: print("Error while processing typehint:", typehint) raise - _typehint = typehint - _typehing_parents = [] - while True: - # Too hard to manage typehints with commas - if "[" not in _typehint: - break - - parts = _typehint.split("[") - parent = parts.pop(0) - - try: - # Test if typehint is valid for known '_api' content - exec(f"_: {parent} = None", api_globals) - except NameError: - _typehint = parent - break - - _typehint = "[".join(parts)[:-1] - if "," in _typehint: - _typing = parent - break - - _typehing_parents.append(parent) - - if _typehing_parents: - typehint = _typehint - for parent in reversed(_typehing_parents): - typehint = f"{parent}[{typehint}]" - return typehint - - return typehint - def _get_param_typehint(param, api_globals): if param.annotation is inspect.Parameter.empty: @@ -455,12 +421,20 @@ def main(): formatting_init_content = prepare_init_without_api(init_filepath) # Read content of first part of `_api.py` to get global variables - # - disable type checking so imports done only during typechecking are - # not executed + # - first with disabled type checking so other files from ayon_api are + # loded without any issues typing.TYPE_CHECKING = False api_globals = {"__name__": "ayon_api._api"} exec(parts[0], api_globals) + # - second with enabled type checking to get all available types in the + # file + # NOTE The file contains 'from __future__ import annotations' so any + # typehints can be used, but we should validate if are available. + typing.TYPE_CHECKING = True + api_globals = {"__name__": "ayon_api._api"} + exec(parts[0], api_globals) + for attr_name in dir(__builtins__): api_globals[attr_name] = getattr(__builtins__, attr_name) From 1e6fde5937ca2f0c063c5c068acbbf10ef14e1d0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:11:40 +0200 Subject: [PATCH 485/506] move 'CreateLinkData' to typing --- ayon_api/_api.py | 2 +- ayon_api/_api_helpers/links.py | 6 +----- ayon_api/typing.py | 4 ++++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index dba95f59f..ba0af2aba 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -51,6 +51,7 @@ EntityListScope, BackgroundOperationTask, LinkDirection, + CreateLinkData, EventFilter, EventStatus, EnrollEventData, @@ -86,7 +87,6 @@ EntityListAttributeDefinitionDict, AdvancedFilterDict, ) - from ._api_helpers.links import CreateLinkData class GlobalServerAPI(ServerAPI): diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index b2b8d0973..a50b21817 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -15,11 +15,7 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from typing import TypedDict - from ayon_api.typing import LinkDirection - - class CreateLinkData(TypedDict): - id: str + from ayon_api.typing import LinkDirection, CreateLinkData class LinksAPI(BaseServerAPI): diff --git a/ayon_api/typing.py b/ayon_api/typing.py index dd1df5085..e620de085 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -128,6 +128,10 @@ class BackgroundOperationTask(TypedDict): LinkDirection = Literal["in", "out"] +class CreateLinkData(TypedDict): + id: str + + class AttributeEnumItemDict(TypedDict): value: Union[str, int, float, bool] label: str From b035208914d63521fc30717fc3f8a56268c5629f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:11:53 +0200 Subject: [PATCH 486/506] fix typehints --- ayon_api/_api.py | 8 ++++---- ayon_api/_api_helpers/lists.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index ba0af2aba..42eac9827 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -8053,8 +8053,8 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. - entity_list_folder_id (str | None | type[NOT_SET]): New - entity list folder id. Use 'None' to move entity list to root. + entity_list_folder_id (str | None | type[NOT_SET]): New entity + list folder id. Use ``None`` to move entity list to root. Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. @@ -8331,7 +8331,7 @@ def create_entity_list_folder( parent_id: str | None = None, color: str | None = None, icon: str | None = None, - scope: list[EntityListScope] | Non] = None, + scope: list[EntityListScope] | None = None, data: dict[str, Any] | None = None, access: dict[str, Any] | None = None, entity_list_folder_id: str | None = None, @@ -8379,7 +8379,7 @@ def update_entity_list_folder( parent_id: str | None | type[NOT_SET] = NOT_SET, color: str | None = None, icon: str | None = None, - scope: list[EntityListScope] | Non] = None, + scope: list[EntityListScope] | None = None, data: dict[str, Any] | None = None, access: dict[str, Any] | None = None, ) -> None: diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 76fa14572..329ccdf66 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -243,8 +243,8 @@ def update_entity_list( entity list. data (Optional[dict[str, Any]]): Custom data of entity list. tags (Optional[list[str]]): Entity list tags. - entity_list_folder_id (str | None | type[NOT_SET]): New - entity list folder id. Use 'None' to move entity list to root. + entity_list_folder_id (str | None | type[NOT_SET]): New entity + list folder id. Use ``None`` to move entity list to root. Use 'NOT_SET' to keep current folder. owner (Optional[str]): New owner of the list. active (Optional[bool]): Change active state of entity list. From b4323d3866b26794f853b385719a072486d79ae7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 12 Jun 2026 10:50:11 +0200 Subject: [PATCH 487/506] do not safe-guard token validation --- ayon_api/server_api.py | 46 ++++++++++++++++++------------------------ 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 7347ca231..ee16d8e86 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -774,32 +774,26 @@ def validate_token(self) -> bool: self.close_session() return False - try: - # TODO add other possible validations - # - existence of 'user' key in info - # - validate that 'site_id' is in 'sites' in info - self._get_server_info() - - user_info = get_user_info_by_token( - self.base_url, - self._token_info.token, - verify=self._ssl_verify, - cert=self._cert, - timeout=self.timeout, - ) - self._token_info.is_valid = user_info.is_valid - self._token_info.unauthorized_response = user_info.response - is_service = None - if user_info.is_valid: - is_service = user_info.is_service - self._token_info.is_service = is_service - - except Exception: - self._token_info.is_valid = False - self._token_info.is_service = None - self._token_info.unauthorized_response = None - self.close_session() - self.log.error("Failed to validate token.", exc_info=True) + # TODO add other possible validations + # - existence of 'user' key in info + # - validate that 'site_id' is in 'sites' in info + + # Check server url + self._get_server_info() + + user_info = get_user_info_by_token( + self.base_url, + self._token_info.token, + verify=self._ssl_verify, + cert=self._cert, + timeout=self.timeout, + ) + self._token_info.is_valid = user_info.is_valid + self._token_info.unauthorized_response = user_info.response + is_service = None + if user_info.is_valid: + is_service = user_info.is_service + self._token_info.is_service = is_service return self._token_info.is_valid From bcfc374c31dc34f3a83bc8b3741a7f97a627a0c8 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 12 Jun 2026 09:17:26 +0000 Subject: [PATCH 488/506] Release version 1.2.21 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 5c0d0e7c3..96c4306d9 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.21-dev" +__version__ = "1.2.21" diff --git a/pyproject.toml b/pyproject.toml index 470293c4d..16f045ef6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.21-dev" +version = "1.2.21" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.21-dev" +version = "1.2.21" description = "AYON Python API" authors = [ "ynput.io " From a405897468e4f1f18f6012b11b26e4b41e0fea64 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Fri, 12 Jun 2026 09:17:47 +0000 Subject: [PATCH 489/506] Bump version to 1.2.22-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 96c4306d9..fa3019b94 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.21" +__version__ = "1.2.22-dev" diff --git a/pyproject.toml b/pyproject.toml index 16f045ef6..27939bd0c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.21" +version = "1.2.22-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.21" +version = "1.2.22-dev" description = "AYON Python API" authors = [ "ynput.io " From 011cdd30f59705b231071b3471ed9aad0b903470 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:08:39 +0200 Subject: [PATCH 490/506] define default value for optional argumet --- ayon_api/_api_helpers/lists.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 329ccdf66..3d9b6967d 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -407,7 +407,7 @@ def update_entity_list_item( list_id: str, item_id: str, *, - new_list_id: Optional[str], + new_list_id: Optional[str] = None, position: Optional[int] = None, label: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, From 83bdcf40e77e9dd47fc9344b7e614ea34978826f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:10:30 +0200 Subject: [PATCH 491/506] fix public function too --- ayon_api/_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 4f0bf7f59..728ce0e59 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -8209,7 +8209,7 @@ def update_entity_list_item( list_id: str, item_id: str, *, - new_list_id: Optional[str], + new_list_id: Optional[str] = None, position: Optional[int] = None, label: Optional[str] = None, attrib: Optional[dict[str, Any]] = None, From cac730b5dd5a97b61d06656f77ec4280f2cd4dda Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:18:43 +0200 Subject: [PATCH 492/506] added as_username as public function --- ayon_api/__init__.py | 2 ++ ayon_api/_api.py | 29 ++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 3d5f89311..2f04c2800 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -31,6 +31,7 @@ set_environments, get_server_api_connection, get_default_settings_variant, + as_username, get_base_url, get_rest_url, get_ssl_verify, @@ -349,6 +350,7 @@ "set_environments", "get_server_api_connection", "get_default_settings_variant", + "as_username", "get_base_url", "get_rest_url", "get_ssl_verify", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 728ce0e59..66ebccec9 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -11,10 +11,11 @@ from __future__ import annotations +from contextlib import contextmanager import os import socket import typing -from typing import Optional, Iterable, Generator, Any +from typing import Optional, Iterable, Generator, Any, ContextManager import requests @@ -411,6 +412,32 @@ def get_default_settings_variant(): return con.get_default_settings_variant() +@contextmanager +def as_username( + username: str | None, + ignore_service_error: bool = False, +) -> ContextManager[None]: + """Service API will temporarily work as other user. + + This method can be used only if service API key is logged in. + + Args: + username (str | None): Username to work as when service. + ignore_service_error (bool): Ignore error when service + API key is not used. + + Raises: + ValueError: When connection is not yet authenticated or api key + is not service token. + + """ + con = get_server_api_connection() + with con.as_username( + username, ignore_service_error=ignore_service_error + ): + yield + + # ------------------------------------------------ # This content is generated automatically. # ------------------------------------------------ From e5779a2701a6874b1e7a708e334003b62a5f0a7a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:52:18 +0200 Subject: [PATCH 493/506] fix custom file id in upload functions --- ayon_api/_api.py | 46 ++++++++++++------------- ayon_api/server_api.py | 78 +++++++++++++++++++++++++----------------- 2 files changed, 68 insertions(+), 56 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 66ebccec9..ac41c9741 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1202,12 +1202,12 @@ def upload_project_file( project_name: str, filepath: str, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, - file_id: Optional[str] = None, - activity_id: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + content_type: str | None = None, + filename: str | None = None, + file_id: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, + **kwargs, ) -> requests.Response: """Upload project file from a filepath. @@ -1218,14 +1218,13 @@ def upload_project_file( Args: project_name (str): Project name. filepath (str): Path where file will be downloaded. - content_type (Optional[str]): MIME type of file. - filename (Optional[str]): Server filename, filename from filepath + content_type (str | None): MIME type of file. + filename (str | None): Server filename, filename from filepath is used if not passed. - file_id (Optional[str]): File id. - activity_id (Optional[str]): To which activity is file related. - chunk_size (Optional[int]): Size of chunks that are received + file_id (str | None): File id. + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1239,9 +1238,9 @@ def upload_project_file( content_type=content_type, filename=filename, file_id=file_id, - activity_id=activity_id, chunk_size=chunk_size, progress=progress, + **kwargs, ) @@ -1250,11 +1249,11 @@ def upload_project_file_from_stream( stream: StreamType, filename: str, *, - content_type: Optional[str] = None, - file_id: Optional[str] = None, - activity_id: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + content_type: str | None = None, + file_id: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, + **kwargs, ) -> requests.Response: """Upload project file from a filepath. @@ -1266,12 +1265,11 @@ def upload_project_file_from_stream( project_name (str): Project name. stream (StreamType): Stream used as source for upload. filename (str): Name of file on server. - content_type (Optional[str]): MIME type of file. - file_id (Optional[str]): File id. - activity_id (Optional[str]): To which activity is file related. - chunk_size (Optional[int]): Size of chunks that are received + content_type (str | None): MIME type of file. + file_id (str | None): File id. + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1285,9 +1283,9 @@ def upload_project_file_from_stream( filename=filename, content_type=content_type, file_id=file_id, - activity_id=activity_id, chunk_size=chunk_size, progress=progress, + **kwargs, ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index ee16d8e86..f13644ce6 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1806,12 +1806,13 @@ def upload_project_file( project_name: str, filepath: str, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, - file_id: Optional[str] = None, - activity_id: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + content_type: str | None = None, + filename: str | None = None, + file_id: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, + # TODO remove when 'activity_id' is handled + **kwargs ) -> requests.Response: """Upload project file from a filepath. @@ -1822,14 +1823,13 @@ def upload_project_file( Args: project_name (str): Project name. filepath (str): Path where file will be downloaded. - content_type (Optional[str]): MIME type of file. - filename (Optional[str]): Server filename, filename from filepath + content_type (str | None): MIME type of file. + filename (str | None): Server filename, filename from filepath is used if not passed. - file_id (Optional[str]): File id. - activity_id (Optional[str]): To which activity is file related. - chunk_size (Optional[int]): Size of chunks that are received + file_id (str | None): File id. + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1844,17 +1844,24 @@ def upload_project_file( if not content_type: content_type = "application/octet-stream" - query = prepare_query_string({ - "x_file_id": file_id, - "x_activity_id": activity_id, - }) + if "activity_id" in kwargs: + self.log.warning( + "DEV WARNING: Uploading file does not support to specify" + " 'activity_id'." + ) + + headers = {} + if file_id: + headers["x-file-id"] = file_id + return self.upload_file( - f"api/projects/{project_name}/files{query}", + f"api/projects/{project_name}/files", filepath, content_type=content_type, filename=filename, chunk_size=chunk_size, progress=progress, + headers=headers, request_type=RequestTypes.post, ) @@ -1864,11 +1871,12 @@ def upload_project_file_from_stream( stream: StreamType, filename: str, *, - content_type: Optional[str] = None, - file_id: Optional[str] = None, - activity_id: Optional[str] = None, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + content_type: str | None = None, + file_id: str | None = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, + # TODO remove when 'activity_id' handling is removed + **kwargs ) -> requests.Response: """Upload project file from a filepath. @@ -1880,12 +1888,11 @@ def upload_project_file_from_stream( project_name (str): Project name. stream (StreamType): Stream used as source for upload. filename (str): Name of file on server. - content_type (Optional[str]): MIME type of file. - file_id (Optional[str]): File id. - activity_id (Optional[str]): To which activity is file related. - chunk_size (Optional[int]): Size of chunks that are received + content_type (str | None): MIME type of file. + file_id (str | None): File id. + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1898,17 +1905,24 @@ def upload_project_file_from_stream( if not content_type: content_type = "application/octet-stream" - query = prepare_query_string({ - "x_file_id": file_id, - "x_activity_id": activity_id, - }) + if "activity_id" in kwargs: + self.log.warning( + "DEV WARNING: Uploading file does not support to specify" + " 'activity_id'." + ) + + headers = {} + if file_id: + headers["x-file-id"] = file_id + return self.upload_file_from_stream( - f"api/projects/{project_name}/files{query}", + f"api/projects/{project_name}/files", stream, content_type=content_type, filename=filename, chunk_size=chunk_size, progress=progress, + headers=headers, request_type=RequestTypes.post, ) From f6d31dea799adc7ff94fefbcc5d7fe3396503ed5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jun 2026 11:59:32 +0200 Subject: [PATCH 494/506] add activity id back --- ayon_api/_api.py | 10 ++++++---- ayon_api/server_api.py | 26 ++++++++++---------------- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index ac41c9741..76e3371ec 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -1205,9 +1205,9 @@ def upload_project_file( content_type: str | None = None, filename: str | None = None, file_id: str | None = None, + activity_id: str | None = None, chunk_size: int | None = None, progress: TransferProgress | None = None, - **kwargs, ) -> requests.Response: """Upload project file from a filepath. @@ -1222,6 +1222,7 @@ def upload_project_file( filename (str | None): Server filename, filename from filepath is used if not passed. file_id (str | None): File id. + activity_id (str | None): To which activity is file related. chunk_size (int | None): Size of chunks that are received in single loop. progress (TransferProgress | None): Object that gives ability @@ -1238,9 +1239,9 @@ def upload_project_file( content_type=content_type, filename=filename, file_id=file_id, + activity_id=activity_id, chunk_size=chunk_size, progress=progress, - **kwargs, ) @@ -1251,9 +1252,9 @@ def upload_project_file_from_stream( *, content_type: str | None = None, file_id: str | None = None, + activity_id: str | None = None, chunk_size: int | None = None, progress: TransferProgress | None = None, - **kwargs, ) -> requests.Response: """Upload project file from a filepath. @@ -1267,6 +1268,7 @@ def upload_project_file_from_stream( filename (str): Name of file on server. content_type (str | None): MIME type of file. file_id (str | None): File id. + activity_id (str | None): To which activity is file related. chunk_size (int | None): Size of chunks that are received in single loop. progress (TransferProgress | None): Object that gives ability @@ -1283,9 +1285,9 @@ def upload_project_file_from_stream( filename=filename, content_type=content_type, file_id=file_id, + activity_id=activity_id, chunk_size=chunk_size, progress=progress, - **kwargs, ) diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index f13644ce6..2fc2dc082 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -1809,10 +1809,9 @@ def upload_project_file( content_type: str | None = None, filename: str | None = None, file_id: str | None = None, + activity_id: str | None = None, chunk_size: int | None = None, progress: TransferProgress | None = None, - # TODO remove when 'activity_id' is handled - **kwargs ) -> requests.Response: """Upload project file from a filepath. @@ -1827,6 +1826,7 @@ def upload_project_file( filename (str | None): Server filename, filename from filepath is used if not passed. file_id (str | None): File id. + activity_id (str | None): To which activity is file related. chunk_size (int | None): Size of chunks that are received in single loop. progress (TransferProgress | None): Object that gives ability @@ -1844,16 +1844,13 @@ def upload_project_file( if not content_type: content_type = "application/octet-stream" - if "activity_id" in kwargs: - self.log.warning( - "DEV WARNING: Uploading file does not support to specify" - " 'activity_id'." - ) - headers = {} if file_id: headers["x-file-id"] = file_id + if activity_id: + headers["x-activity-id"] = activity_id + return self.upload_file( f"api/projects/{project_name}/files", filepath, @@ -1873,10 +1870,9 @@ def upload_project_file_from_stream( *, content_type: str | None = None, file_id: str | None = None, + activity_id: str | None = None, chunk_size: int | None = None, progress: TransferProgress | None = None, - # TODO remove when 'activity_id' handling is removed - **kwargs ) -> requests.Response: """Upload project file from a filepath. @@ -1890,6 +1886,7 @@ def upload_project_file_from_stream( filename (str): Name of file on server. content_type (str | None): MIME type of file. file_id (str | None): File id. + activity_id (str | None): To which activity is file related. chunk_size (int | None): Size of chunks that are received in single loop. progress (TransferProgress | None): Object that gives ability @@ -1905,16 +1902,13 @@ def upload_project_file_from_stream( if not content_type: content_type = "application/octet-stream" - if "activity_id" in kwargs: - self.log.warning( - "DEV WARNING: Uploading file does not support to specify" - " 'activity_id'." - ) - headers = {} if file_id: headers["x-file-id"] = file_id + if activity_id: + headers["x-activity-id"] = activity_id + return self.upload_file_from_stream( f"api/projects/{project_name}/files", stream, From 2c4367cd81ea9acfa8060d6212e1a8529a0a885c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 22 Jun 2026 12:07:11 +0200 Subject: [PATCH 495/506] update type hints --- ayon_api/_api.py | 250 +++++++++++++------------ ayon_api/graphql.py | 26 +-- ayon_api/server_api.py | 402 +++++++++++++++++++++-------------------- ayon_api/typing.py | 69 ++++--- 4 files changed, 377 insertions(+), 370 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 76e3371ec..2bf8f2025 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -441,21 +441,21 @@ def as_username( # ------------------------------------------------ # This content is generated automatically. # ------------------------------------------------ -def get_base_url(): +def get_base_url() -> str: con = get_server_api_connection() return con.get_base_url() -def get_rest_url(): +def get_rest_url() -> str: con = get_server_api_connection() return con.get_rest_url() -def get_ssl_verify(): +def get_ssl_verify() -> bool | str | None: """Enable ssl verification. Returns: - bool: Current state of ssl verification. + bool | str | None: Current state of ssl verification. """ con = get_server_api_connection() @@ -463,12 +463,12 @@ def get_ssl_verify(): def set_ssl_verify( - ssl_verify, -): + ssl_verify: bool | str | None, +) -> None: """Change ssl verification state. Args: - ssl_verify (Union[bool, str, None]): Enabled/disable + ssl_verify (bool | str | None): Enabled/disable ssl verification, can be a path to file. """ @@ -478,11 +478,11 @@ def set_ssl_verify( ) -def get_cert(): +def get_cert() -> str | None: """Current cert file used for connection to server. Returns: - Union[str, None]: Path to cert file. + str | None: Path to cert file. """ con = get_server_api_connection() @@ -490,12 +490,12 @@ def get_cert(): def set_cert( - cert, -): + cert: str | None, +) -> None: """Change cert file used for connection to server. Args: - cert (Union[str, None]): Path to cert file. + cert (str | None): Path to cert file. """ con = get_server_api_connection() @@ -516,12 +516,12 @@ def get_timeout() -> float: def set_timeout( - timeout: Optional[float], -): + timeout: int | float | None, +) -> None: """Change timeout value for requests. Args: - timeout (Optional[float]): Timeout value in seconds. + timeout (float | None): Timeout value in seconds. """ con = get_server_api_connection() @@ -542,12 +542,12 @@ def get_max_retries() -> int: def set_max_retries( - max_retries: Optional[int], -): + max_retries: int | None, +) -> None: """Change max retries value for requests. Args: - max_retries (Optional[int]): Max retries value. + max_retries (int | None): Max retries value. """ con = get_server_api_connection() @@ -567,14 +567,14 @@ def is_service_user() -> bool: return con.is_service_user() -def get_site_id() -> Optional[str]: +def get_site_id() -> str | None: """Site id used for connection. Site id tells server from which machine/site is connection created and is used for default site overrides when settings are received. Returns: - Optional[str]: Site id value or None if not filled. + str | None: Site id value or None if not filled. """ con = get_server_api_connection() @@ -582,15 +582,15 @@ def get_site_id() -> Optional[str]: def set_site_id( - site_id: Optional[str], -): + site_id: str | None, +) -> None: """Change site id of connection. Behave as specific site for server. It affects default behavior of settings getter methods. Args: - site_id (Optional[str]): Site id value, or 'None' to unset. + site_id (str | None): Site id value, or 'None' to unset. """ con = get_server_api_connection() @@ -599,7 +599,7 @@ def set_site_id( ) -def get_client_version() -> Optional[str]: +def get_client_version() -> str | None: """Version of client used to connect to server. Client version is AYON client build desktop application. @@ -613,14 +613,14 @@ def get_client_version() -> Optional[str]: def set_client_version( - client_version: Optional[str], -): + client_version: str | None, +) -> None: """Set version of client used to connect to server. Client version is AYON client build desktop application. Args: - client_version (Optional[str]): Client version string. + client_version (str | None): Client version string. """ con = get_server_api_connection() @@ -631,7 +631,7 @@ def set_client_version( def set_default_settings_variant( variant: str, -): +) -> None: """Change default variant for addon settings. Note: @@ -649,11 +649,11 @@ def set_default_settings_variant( ) -def get_sender() -> str: +def get_sender() -> str | None: """Sender used to send requests. Returns: - Union[str, None]: Sender name or None. + str | None: Sender name or None. """ con = get_server_api_connection() @@ -661,12 +661,12 @@ def get_sender() -> str: def set_sender( - sender: Optional[str], -): + sender: str | None, +) -> None: """Change sender used for requests. Args: - sender (Optional[str]): Sender name or None. + sender (str | None): Sender name or None. """ con = get_server_api_connection() @@ -675,13 +675,13 @@ def set_sender( ) -def get_sender_type() -> Optional[str]: +def get_sender_type() -> str | None: """Sender type used to send requests. Sender type is supported since AYON server 1.5.5 . Returns: - Optional[str]: Sender type or None. + str | None: Sender type or None. """ con = get_server_api_connection() @@ -689,12 +689,12 @@ def get_sender_type() -> Optional[str]: def set_sender_type( - sender_type: Optional[str], -): + sender_type: str | None, +) -> None: """Change sender type used for requests. Args: - sender_type (Optional[str]): Sender type or None. + sender_type (str | None): Sender type or None. """ con = get_server_api_connection() @@ -764,10 +764,10 @@ def links_graphql_support_data() -> bool: def get_users( - project_name: Optional[str] = None, - usernames: Optional[Iterable[str]] = None, - emails: Optional[Iterable[str]] = None, - fields: Optional[Iterable[str]] = None, + project_name: str | None = None, + usernames: Iterable[str] | None = None, + emails: Iterable[str] | None = None, + fields: Iterable[str] | None = None, ) -> Generator[dict[str, Any], None, None]: """Get Users. @@ -775,14 +775,14 @@ def get_users( it is required to pass in 'project_name' filter. Args: - project_name (Optional[str]): Project name. - usernames (Optional[Iterable[str]]): Filter by usernames. - emails (Optional[Iterable[str]]): Filter by emails. - fields (Optional[Iterable[str]]): Fields to be queried + project_name (str | None): Project name. + usernames (Iterable[str] | None): Filter by usernames. + emails (Iterable[str] | None): Filter by emails. + fields (Iterable[str] | None): Fields to be queried for users. Returns: - Generator[dict[str, Any]]: Queried users. + Generator[dict[str, Any], None, None]: Queried users. """ con = get_server_api_connection() @@ -796,9 +796,9 @@ def get_users( def get_user_by_name( username: str, - project_name: Optional[str] = None, - fields: Optional[Iterable[str]] = None, -) -> Optional[dict[str, Any]]: + project_name: str | None = None, + fields: Iterable[str] | None = None, +) -> dict[str, Any] | None: """Get user by name using GraphQl. Only administrators and managers can fetch all users. For other users @@ -806,13 +806,11 @@ def get_user_by_name( Args: username (str): Username. - project_name (Optional[str]): Define scope of project. - fields (Optional[Iterable[str]]): Fields to be queried - for users. + project_name (str | None): Define scope of project. + fields (Iterable[str] | None): Fields to be queried for users. Returns: - Union[dict[str, Any], None]: User info or None if user is not - found. + dict[str, Any] | None: User info or None if user is not found. """ con = get_server_api_connection() @@ -824,17 +822,17 @@ def get_user_by_name( def get_user( - username: Optional[str] = None, -) -> Optional[dict[str, Any]]: + username: str | None = None, +) -> dict[str, Any] | None: """Get user info using REST endpoint. User contains only explicitly set attributes in 'attrib'. Args: - username (Optional[str]): Username. + username (str | None): Username. Returns: - Optional[dict[str, Any]]: User info or None if user is not + dict[str, Any] | None: User info or None if user is not found. """ @@ -847,7 +845,7 @@ def get_user( def raw_post( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.raw_post( entrypoint=entrypoint, @@ -858,7 +856,7 @@ def raw_post( def raw_put( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.raw_put( entrypoint=entrypoint, @@ -869,7 +867,7 @@ def raw_put( def raw_patch( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.raw_patch( entrypoint=entrypoint, @@ -880,7 +878,7 @@ def raw_patch( def raw_get( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.raw_get( entrypoint=entrypoint, @@ -891,7 +889,7 @@ def raw_get( def raw_delete( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.raw_delete( entrypoint=entrypoint, @@ -902,7 +900,7 @@ def raw_delete( def post( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.post( entrypoint=entrypoint, @@ -913,7 +911,7 @@ def post( def put( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.put( entrypoint=entrypoint, @@ -924,7 +922,7 @@ def put( def patch( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.patch( entrypoint=entrypoint, @@ -935,7 +933,7 @@ def patch( def get( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.get( entrypoint=entrypoint, @@ -946,7 +944,7 @@ def get( def delete( entrypoint: str, **kwargs, -): +) -> RestApiResponse: con = get_server_api_connection() return con.delete( entrypoint=entrypoint, @@ -954,7 +952,7 @@ def delete( ) -def get_server_config(): +def get_server_config() -> dict[str, Any]: con = get_server_api_connection() return con.get_server_config() @@ -976,14 +974,14 @@ def set_server_config( ) -def get_server_config_overrides(): +def get_server_config_overrides() -> dict[str, Any]: con = get_server_api_connection() return con.get_server_config_overrides() def get_server_config_value( key: str, -): +) -> Any: con = get_server_api_connection() return con.get_server_config_value( key=key, @@ -994,8 +992,8 @@ def download_server_config_file( file_type: Literal["login_background", "studio_logo"], filepath: str, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download server config file. @@ -1024,8 +1022,8 @@ def download_server_config_file_to_stream( file_type: Literal["login_background", "studio_logo"], stream: StreamType, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download server config file to byte stream. @@ -1128,8 +1126,8 @@ def upload_server_config_file_from_stream( def download_file_to_stream( endpoint: str, stream: StreamType, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download file from AYON server to IOStream. @@ -1147,9 +1145,9 @@ def download_file_to_stream( endpoint (str): Endpoint or URL to file that should be downloaded. stream (StreamType): Stream where output will be stored. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. """ @@ -1165,8 +1163,8 @@ def download_file_to_stream( def download_file( endpoint: str, filepath: str, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download file from AYON server. @@ -1183,9 +1181,9 @@ def download_file( Args: endpoint (str): Endpoint or URL to file that should be downloaded. filepath (str): Path where file will be downloaded. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. """ @@ -1296,8 +1294,8 @@ def download_project_file( file_id: str, filepath: str, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download project file to filepath. @@ -1309,9 +1307,9 @@ def download_project_file( project_name (str): Project name. file_id (str): File id. filepath (str): Path where file will be downloaded. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1333,8 +1331,8 @@ def download_project_file_to_stream( file_id: str, stream: StreamType, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download project file to a stream. @@ -1346,9 +1344,9 @@ def download_project_file_to_stream( project_name (str): Project name. file_id (str): File id. stream (StreamType): Stream where output will be stored. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1381,11 +1379,11 @@ def delete_project_file( def upload_file_from_stream( endpoint: str, stream: StreamType, - progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, + progress: TransferProgress | None = None, + request_type: RequestType | None = None, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, + content_type: str | None = None, + filename: str | None = None, **kwargs, ) -> requests.Response: """Upload file to server from bytes. @@ -1397,12 +1395,12 @@ def upload_file_from_stream( Args: endpoint (str): Endpoint or url where file will be uploaded. stream (StreamType): File content stream. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track upload progress. - request_type (Optional[RequestType]): Type of request that will + request_type (RequestType | None): Type of request that will be used to upload file. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): Filename of file on server. + content_type (str | None): MIME type of the file. + filename (str | None): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1425,11 +1423,11 @@ def upload_file_from_stream( def upload_file( endpoint: str, filepath: str, - progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, + progress: TransferProgress | None = None, + request_type: RequestType | None = None, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, + content_type: str | None = None, + filename: str | None = None, **kwargs, ) -> requests.Response: """Upload file to server. @@ -1441,12 +1439,12 @@ def upload_file( Args: endpoint (str): Endpoint or url where file will be uploaded. filepath (str): Source filepath. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track upload progress. - request_type (Optional[RequestType]): Type of request that will + request_type (RequestType | None): Type of request that will be used to upload file. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): Filename of file on server. + content_type (str | None): MIME type of the file. + filename (str | None): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -1470,10 +1468,10 @@ def upload_reviewable( project_name: str, version_id: str, filepath: str, - label: Optional[str] = None, - content_type: Optional[str] = None, - filename: Optional[str] = None, - progress: Optional[TransferProgress] = None, + label: str | None = None, + content_type: str | None = None, + filename: str | None = None, + progress: TransferProgress | None = None, **kwargs, ) -> requests.Response: """Upload reviewable file to server. @@ -1482,12 +1480,12 @@ def upload_reviewable( project_name (str): Project name. version_id (str): Version id. filepath (str): Reviewable file path to upload. - label (Optional[str]): Reviewable label. Filled automatically + label (str | None): Reviewable label. Filled automatically server side with filename. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): User as original filename. Filename from + content_type (str | None): MIME type of the file. + filename (str | None): User as original filename. Filename from 'filepath' is used when not filled. - progress (Optional[TransferProgress]): Progress. + progress (TransferProgress | None): Progress. Returns: requests.Response: Server response. @@ -1506,7 +1504,7 @@ def upload_reviewable( ) -def trigger_server_restart(): +def trigger_server_restart() -> None: """Trigger server restart. Restart may be required when a change of specific value happened on @@ -1519,13 +1517,13 @@ def trigger_server_restart(): def query_graphql( query: str, - variables: Optional[dict[str, Any]] = None, + variables: dict[str, Any] | None = None, ) -> GraphQlResponse: """Execute GraphQl query. Args: query (str): GraphQl query string. - variables (Optional[dict[str, Any]): Variables that can be + variables (dict[str, Any] | None): Variables that can be used in query. Returns: @@ -1544,7 +1542,7 @@ def get_graphql_schema() -> dict[str, Any]: return con.get_graphql_schema() -def get_server_schema() -> Optional[dict[str, Any]]: +def get_server_schema() -> dict[str, Any] | None: """Get server schema with info, url paths, components etc. Todos: @@ -1598,7 +1596,7 @@ def get_rest_entity_by_id( project_name: str, entity_type: str, entity_id: str, -) -> Optional[AnyEntityDict]: +) -> AnyEntityDict | None: """Get entity using REST on a project by its id. Args: @@ -1608,7 +1606,7 @@ def get_rest_entity_by_id( entity_id (str): Id of entity. Returns: - Optional[AnyEntityDict]: Received entity data. + AnyEntityDict | None: Received entity data. """ con = get_server_api_connection() @@ -1635,9 +1633,9 @@ def send_batch_operations( 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 + can_fail (bool): Server will try to process all operations even if one of them fails. - raise_on_fail (Optional[bool]): Raise exception if an operation + raise_on_fail (bool): Raise exception if an operation fails. You can handle failed operations on your own when set to 'False'. @@ -1686,10 +1684,10 @@ def send_background_batch_operations( 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 + can_fail (bool): Server will try to process all operations even if one of them fails. wait (bool): Wait for operations to end. - raise_on_fail (Optional[bool]): Raise exception if an operation + raise_on_fail (bool): Raise exception if an operation fails. You can handle failed operations on your own when set to 'False'. Used when 'wait' is enabled. diff --git a/ayon_api/graphql.py b/ayon_api/graphql.py index 8d0cb05c1..4feed17a0 100644 --- a/ayon_api/graphql.py +++ b/ayon_api/graphql.py @@ -4,7 +4,7 @@ import numbers from abc import ABC, abstractmethod import typing -from typing import Optional, Iterable, Any, Generator +from typing import Iterable, Any, Generator from .exceptions import GraphQlQueryError, GraphQlQueryFailed from .utils import SortOrder @@ -17,7 +17,7 @@ FIELD_VALUE = object() -def fields_to_dict(fields: Optional[Iterable[str]]) -> dict: +def fields_to_dict(fields: Iterable[str] | None) -> dict: output = {} if not fields: return output @@ -85,7 +85,7 @@ class GraphQlQuery: """ offset = 2 - def __init__(self, name: str, order: Optional[int] = None) -> None: + def __init__(self, name: str, order: int | None = None) -> None: self._name = name self._variables = {} self._children = [] @@ -140,7 +140,7 @@ def has_multiple_edge_fields(self) -> bool: return self._has_multiple_edge_fields def add_variable( - self, key: str, value_type: str, value: Optional[Any] = None + self, key: str, value_type: str, value: Any | None = None ) -> QueryVariable: """Add variable to query. @@ -185,7 +185,7 @@ def get_variable(self, key: str) -> QueryVariable: return self._variables[key]["variable"] def get_variable_value( - self, key: str, default: Optional[Any] = None + self, key: str, default: Any | None = None ) -> Any: """Get Current value of variable. @@ -281,7 +281,7 @@ def add_field(self, name: str) -> GraphQlQueryField: def get_field_by_keys( self, keys: Iterable[str] - ) -> Optional[BaseGraphQlQueryField]: + ) -> BaseGraphQlQueryField | None: keys = list(keys) if not keys: return None @@ -294,7 +294,7 @@ def get_field_by_keys( def get_field_by_path( self, path: str - ) -> Optional[BaseGraphQlQueryField]: + ) -> BaseGraphQlQueryField | None: return self.get_field_by_keys(path.split("/")) def calculate_query(self) -> str: @@ -469,7 +469,7 @@ def get_name(self) -> str: def get_field_by_keys( self, keys: Iterable[str] - ) -> Optional[BaseGraphQlQueryField]: + ) -> BaseGraphQlQueryField | None: keys = list(keys) if not keys: return self @@ -480,7 +480,7 @@ def get_field_by_keys( return child.get_field_by_keys(keys) return None - def set_limit(self, limit: Optional[int]) -> None: + def set_limit(self, limit: int | None) -> None: self._limit = limit def set_order(self, order: SortOrder) -> None: @@ -504,7 +504,7 @@ def add_variable( self, key: str, value_type: str, - value: Optional[Any] = None, + value: Any | None = None, ) -> QueryVariable: """Add variable to query. @@ -563,7 +563,7 @@ def _children_iter(self) -> Generator[BaseGraphQlQueryField, None, None]: for child in self._children: yield child - def sum_edge_fields(self, max_limit: Optional[int] = None) -> int: + def sum_edge_fields(self, max_limit: int | None = None) -> int: """Check how many edge fields query has. In case there are multiple edge fields or are nested the query can't @@ -637,7 +637,7 @@ def reset_cursor(self) -> None: child.reset_cursor() def get_variable_value( - self, key: str, default: Optional[Any] = None + self, key: str, default: Any | None = None ) -> Any: return self._query_item.get_variable_value(key, default) @@ -678,7 +678,7 @@ def add_field(self, name: str) -> GraphQlQueryField: self.add_obj_field(item) return item - def _filter_value_to_str(self, value: Any) -> Optional[str]: + def _filter_value_to_str(self, value: Any) -> str | None: if isinstance(value, QueryVariable): if self.get_variable_value(value.variable_name) is None: return None diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 2fc2dc082..e27bcd496 100644 --- a/ayon_api/server_api.py +++ b/ayon_api/server_api.py @@ -17,7 +17,9 @@ import uuid from contextlib import contextmanager import typing -from typing import Optional, Iterable, Generator, Any, Union, Literal +from typing import ( + Iterable, Generator, Any, Literal, ContextManager +) import requests @@ -168,23 +170,23 @@ def clear(self): self._default_user = None @property - def username(self) -> Optional[str]: + def username(self) -> str | None: # Use '_user_ids' for boolean check to have ability "unset" # default user if self._user_ids: return self._last_user return self._default_user - def get_default_username(self) -> Optional[str]: + def get_default_username(self) -> str | None: return self._default_user - def set_default_username(self, username: Optional[str] = None) -> None: + def set_default_username(self, username: str | None = None) -> None: self._default_user = username default_username = property(get_default_username, set_default_username) @contextmanager - def as_user(self, username: Optional[str]) -> Generator[None, None, None]: + def as_user(self, username: str | None) -> ContextManager[None]: self._last_user = username user_id = uuid.uuid4().hex self._user_ids.append(user_id) @@ -251,28 +253,28 @@ class ServerAPI( Args: base_url (str): Example: http://localhost:5000 - token (Optional[str]): Access token (api key) to server. - site_id (Optional[str]): Unique name of site. Should be the same when + token (str | None): Access token (api key) to server. + site_id (str | None): Unique name of site. Should be the same when connection is created from the same machine under same user. - client_version (Optional[str]): Version of client application (used in + client_version (str | None): Version of client application (used in desktop client application). - default_settings_variant (Optional[Literal["production", "staging"]]): + default_settings_variant (Literal["production", "staging"] | None): Settings variant used by default if a method for settings won't get any (by default is 'production'). - sender_type (Optional[str]): Sender type of requests. Used in server + sender_type (str | None): Sender type of requests. Used in server logs and propagated into events. - sender (Optional[str]): Sender of requests, more specific than + sender (str | None): Sender of requests, more specific than sender type (e.g. machine name). Used in server logs and propagated into events. - ssl_verify (Optional[Union[bool, str]]): Verify SSL certificate + ssl_verify (bool | str | None): Verify SSL certificate Looks for env variable value ``AYON_CA_FILE`` by default. If not available then 'True' is used. - cert (Optional[str]): Path to certificate file. Looks for env + cert (str | None): Path to certificate file. Looks for env variable value ``AYON_CERT_FILE`` by default. - create_session (Optional[bool]): Create session for connection if + create_session (bool): Create session for connection if token is available. Default is True. - timeout (Optional[float]): Timeout for requests. - max_retries (Optional[int]): Number of retries for requests. + timeout (float | None): Timeout for requests. + max_retries (int | None): Number of retries for requests. """ _default_max_retries = 3 @@ -284,18 +286,18 @@ class ServerAPI( def __init__( self, base_url: str, - token: Optional[str] = None, - site_id: Optional[str] = NOT_SET, - client_version: Optional[str] = None, - default_settings_variant: Optional[str] = None, - sender_type: Optional[str] = None, - sender: Optional[str] = None, - ssl_verify: Optional[Union[bool, str]] = None, - cert: Optional[str] = None, + token: str | None = None, + site_id: str | None = NOT_SET, + client_version: str | None = None, + default_settings_variant: str | None = None, + sender_type: str | None = None, + sender: str | None = None, + ssl_verify: bool | str | None = None, + cert: str | None = None, create_session: bool = True, - timeout: Optional[float] = None, - max_retries: Optional[int] = None, - ): + timeout: float | None = None, + max_retries: int | None = None, + ) -> None: if not base_url: raise ValueError(f"Invalid server URL {str(base_url)}") @@ -308,14 +310,14 @@ def __init__( # Allow to have 'site_id' to 'None' if site_id is NOT_SET: site_id = get_default_site_id() - self._site_id: Optional[str] = site_id - self._client_version: Optional[str] = client_version + self._site_id: str | None = site_id + self._client_version: str | None = client_version self._default_settings_variant: str = ( default_settings_variant or get_default_settings_variant() ) - self._sender: Optional[str] = sender - self._sender_type: Optional[str] = sender_type + self._sender: str | None = sender + self._sender_type: str | None = sender_type self._timeout: float = 0.0 self._max_retries: int = 0 @@ -336,15 +338,15 @@ def __init__( self._ssl_verify = ssl_verify self._cert = cert - self._token_info = TokenInfo(token=token) + self._token_info: TokenInfo = TokenInfo(token=token) self._server_available = None self._server_version = None self._server_version_tuple = None - self._graphql_allows_traits_in_representations: Optional[bool] = None - self._product_base_type_supported = None - self._links_graphql_support_data = None + self._graphql_allows_traits_in_representations: bool | None = None + self._product_base_type_supported: bool | None = None + self._links_graphql_support_data: bool | None = None self._session = None @@ -361,7 +363,7 @@ def __init__( self._attributes_schema = None self._entity_type_attributes_cache = {} - self._as_user_stack = _AsUserStack() + self._as_user_stack: _AsUserStack = _AsUserStack() # Create session if self._token_info.token and create_session: @@ -372,29 +374,31 @@ def __init__( def log(self) -> logging.Logger: return self._log - def get_base_url(self): + def get_base_url(self) -> str: return self._base_url - def get_rest_url(self): + def get_rest_url(self) -> str: return self._rest_url base_url = property(get_base_url) rest_url = property(get_rest_url) - def get_ssl_verify(self): + def get_ssl_verify(self) -> bool | str | None: """Enable ssl verification. Returns: - bool: Current state of ssl verification. + bool | str | None: Current state of ssl verification. """ return self._ssl_verify - def set_ssl_verify(self, ssl_verify): + def set_ssl_verify( + self, ssl_verify: bool | str | None + ) -> None: """Change ssl verification state. Args: - ssl_verify (Union[bool, str, None]): Enabled/disable + ssl_verify (bool | str | None): Enabled/disable ssl verification, can be a path to file. """ @@ -404,20 +408,20 @@ def set_ssl_verify(self, ssl_verify): if self._session is not None: self._session.verify = ssl_verify - def get_cert(self): + def get_cert(self) -> str | None: """Current cert file used for connection to server. Returns: - Union[str, None]: Path to cert file. + str | None: Path to cert file. """ return self._cert - def set_cert(self, cert): + def set_cert(self, cert: str | None) -> None: """Change cert file used for connection to server. Args: - cert (Union[str, None]): Path to cert file. + cert (str | None): Path to cert file. """ if cert == self._cert: @@ -430,7 +434,7 @@ def set_cert(self, cert): cert = property(get_cert, set_cert) @classmethod - def get_default_timeout(cls): + def get_default_timeout(cls) -> float: """Default value for requests timeout. Utils function 'get_default_timeout' is used by default. @@ -442,7 +446,7 @@ def get_default_timeout(cls): return get_default_timeout() @classmethod - def get_default_max_retries(cls): + def get_default_max_retries(cls) -> int: """Default value for requests max retries. First looks for environment variable SERVER_RETRIES_ENV_KEY, which @@ -469,11 +473,11 @@ def get_timeout(self) -> float: """ return self._timeout - def set_timeout(self, timeout: Optional[float]): + def set_timeout(self, timeout: int | float | None) -> None: """Change timeout value for requests. Args: - timeout (Optional[float]): Timeout value in seconds. + timeout (float | None): Timeout value in seconds. """ if timeout is None: @@ -489,11 +493,11 @@ def get_max_retries(self) -> int: """ return self._max_retries - def set_max_retries(self, max_retries: Optional[int]): + def set_max_retries(self, max_retries: int | None) -> None: """Change max retries value for requests. Args: - max_retries (Optional[int]): Max retries value. + max_retries (int | None): Max retries value. """ if max_retries is None: @@ -504,11 +508,11 @@ def set_max_retries(self, max_retries: Optional[int]): max_retries = property(get_max_retries, set_max_retries) @property - def access_token(self) -> Optional[str]: + def access_token(self) -> str | None: """Access token used for authorization to server. Returns: - Optional[str]: Token string or None if not authorized yet. + str | None: Token string or None if not authorized yet. """ return self._token_info.token @@ -524,26 +528,26 @@ def is_service_user(self) -> bool: raise ValueError("User is not logged in.") return bool(self._token_info.is_service) - def get_site_id(self) -> Optional[str]: + def get_site_id(self) -> str | None: """Site id used for connection. Site id tells server from which machine/site is connection created and is used for default site overrides when settings are received. Returns: - Optional[str]: Site id value or None if not filled. + str | None: Site id value or None if not filled. """ return self._site_id - def set_site_id(self, site_id: Optional[str]): + def set_site_id(self, site_id: str | None) -> None: """Change site id of connection. Behave as specific site for server. It affects default behavior of settings getter methods. Args: - site_id (Optional[str]): Site id value, or 'None' to unset. + site_id (str | None): Site id value, or 'None' to unset. """ if self._site_id == site_id: @@ -554,7 +558,7 @@ def set_site_id(self, site_id: Optional[str]): site_id = property(get_site_id, set_site_id) - def get_client_version(self) -> Optional[str]: + def get_client_version(self) -> str | None: """Version of client used to connect to server. Client version is AYON client build desktop application. @@ -565,13 +569,13 @@ def get_client_version(self) -> Optional[str]: """ return self._client_version - def set_client_version(self, client_version: Optional[str]): + def set_client_version(self, client_version: str | None) -> None: """Set version of client used to connect to server. Client version is AYON client build desktop application. Args: - client_version (Optional[str]): Client version string. + client_version (str | None): Client version string. """ if self._client_version == client_version: @@ -582,16 +586,16 @@ def set_client_version(self, client_version: Optional[str]): client_version = property(get_client_version, set_client_version) - def get_default_settings_variant(self) -> str: + def get_default_settings_variant(self) -> str | None: """Default variant used for settings. Returns: - Union[str, None]: name of variant or None. + str | None: name of variant or None. """ return self._default_settings_variant - def set_default_settings_variant(self, variant: str): + def set_default_settings_variant(self, variant: str) -> None: """Change default variant for addon settings. Note: @@ -610,20 +614,20 @@ def set_default_settings_variant(self, variant: str): set_default_settings_variant ) - def get_sender(self) -> str: + def get_sender(self) -> str | None: """Sender used to send requests. Returns: - Union[str, None]: Sender name or None. + str | None: Sender name or None. """ return self._sender - def set_sender(self, sender: Optional[str]): + def set_sender(self, sender: str | None) -> None: """Change sender used for requests. Args: - sender (Optional[str]): Sender name or None. + sender (str | None): Sender name or None. """ if sender == self._sender: @@ -633,22 +637,22 @@ def set_sender(self, sender: Optional[str]): sender = property(get_sender, set_sender) - def get_sender_type(self) -> Optional[str]: + def get_sender_type(self) -> str | None: """Sender type used to send requests. Sender type is supported since AYON server 1.5.5 . Returns: - Optional[str]: Sender type or None. + str | None: Sender type or None. """ return self._sender_type - def set_sender_type(self, sender_type: Optional[str]): + def set_sender_type(self, sender_type: str | None) -> None: """Change sender type used for requests. Args: - sender_type (Optional[str]): Sender type or None. + sender_type (str | None): Sender type or None. """ if sender_type == self._sender_type: @@ -658,16 +662,18 @@ def set_sender_type(self, sender_type: Optional[str]): sender_type = property(get_sender_type, set_sender_type) - def get_default_service_username(self) -> Optional[str]: + def get_default_service_username(self) -> str | None: """Default username used for callbacks when used with service API key. Returns: - Union[str, None]: Username if any was filled. + str | None: Username if any was filled. """ return self._as_user_stack.get_default_username() - def set_default_service_username(self, username: Optional[str] = None): + def set_default_service_username( + self, username: str | None = None + ) -> None: """Service API will work as other user. Service API keys can work as other user. It can be temporary using @@ -675,7 +681,7 @@ def set_default_service_username(self, username: Optional[str] = None): 'as_user' context manager is not entered. Args: - username (Optional[str]): Username to work as when service. + username (str | None): Username to work as when service. Raises: ValueError: When connection is not yet authenticated or api key @@ -703,16 +709,16 @@ def set_default_service_username(self, username: Optional[str] = None): @contextmanager def as_username( self, - username: Optional[str], + username: str | None, ignore_service_error: bool = False, - ): + ) -> ContextManager[None]: """Service API will temporarily work as other user. This method can be used only if service API key is logged in. Args: - username (Optional[str]): Username to work as when service. - ignore_service_error (Optional[bool]): Ignore error when service + username (str | None): Username to work as when service. + ignore_service_error (bool): Ignore error when service API key is not used. Raises: @@ -727,16 +733,16 @@ def as_username( if not self._token_info.is_service: if ignore_service_error: - yield None + yield return raise ValueError( "Can't set service username. API key is not a service token." ) try: - with self._as_user_stack.as_user(username) as o: + with self._as_user_stack.as_user(username): self._update_session_headers() - yield o + yield finally: self._update_session_headers() @@ -761,7 +767,7 @@ def has_valid_token(self) -> bool: self.validate_token() return self._token_info.is_valid - def validate_server_availability(self): + def validate_server_availability(self) -> None: if not self.is_server_available: raise ServerNotReached( f"Server \"{self._base_url}\" can't be reached" @@ -797,12 +803,12 @@ def validate_token(self) -> bool: return self._token_info.is_valid - def set_token(self, token: Optional[str]): + def set_token(self, token: str | None) -> None: self.reset_token() self._token_info.token = token self.validate_token() - def reset_token(self): + def reset_token(self) -> None: self._token_info.token = None self._token_info.is_service = None self._token_info.is_valid = None @@ -811,7 +817,7 @@ def reset_token(self): def create_session( self, ignore_existing: bool = True, force: bool = False - ): + ) -> None: """Create a connection session. Session helps to keep connection with server without @@ -850,7 +856,7 @@ def create_session( } self._session = session - def close_session(self): + def close_session(self) -> None: if self._session is None: return @@ -859,7 +865,7 @@ def close_session(self): self._session_functions_mapping = {} session.close() - def _update_session_headers(self): + def _update_session_headers(self) -> None: if self._session is None: return @@ -973,10 +979,10 @@ def links_graphql_support_data(self) -> bool: def get_users( self, - project_name: Optional[str] = None, - usernames: Optional[Iterable[str]] = None, - emails: Optional[Iterable[str]] = None, - fields: Optional[Iterable[str]] = None, + project_name: str | None = None, + usernames: Iterable[str] | None = None, + emails: Iterable[str] | None = None, + fields: Iterable[str] | None = None, ) -> Generator[dict[str, Any], None, None]: """Get Users. @@ -984,14 +990,14 @@ def get_users( it is required to pass in 'project_name' filter. Args: - project_name (Optional[str]): Project name. - usernames (Optional[Iterable[str]]): Filter by usernames. - emails (Optional[Iterable[str]]): Filter by emails. - fields (Optional[Iterable[str]]): Fields to be queried + project_name (str | None): Project name. + usernames (Iterable[str] | None): Filter by usernames. + emails (Iterable[str] | None): Filter by emails. + fields (Iterable[str] | None): Fields to be queried for users. Returns: - Generator[dict[str, Any]]: Queried users. + Generator[dict[str, Any], None, None]: Queried users. """ filters = {} @@ -1058,9 +1064,9 @@ def get_users( def get_user_by_name( self, username: str, - project_name: Optional[str] = None, - fields: Optional[Iterable[str]] = None, - ) -> Optional[dict[str, Any]]: + project_name: str | None = None, + fields: Iterable[str] | None = None, + ) -> dict[str, Any] | None: """Get user by name using GraphQl. Only administrators and managers can fetch all users. For other users @@ -1068,13 +1074,11 @@ def get_user_by_name( Args: username (str): Username. - project_name (Optional[str]): Define scope of project. - fields (Optional[Iterable[str]]): Fields to be queried - for users. + project_name (str | None): Define scope of project. + fields (Iterable[str] | None): Fields to be queried for users. Returns: - Union[dict[str, Any], None]: User info or None if user is not - found. + dict[str, Any] | None: User info or None if user is not found. """ if not username: @@ -1089,17 +1093,17 @@ def get_user_by_name( return None def get_user( - self, username: Optional[str] = None - ) -> Optional[dict[str, Any]]: + self, username: str | None = None + ) -> dict[str, Any] | None: """Get user info using REST endpoint. User contains only explicitly set attributes in 'attrib'. Args: - username (Optional[str]): Username. + username (str | None): Username. Returns: - Optional[dict[str, Any]]: User info or None if user is not + dict[str, Any] | None: User info or None if user is not found. """ @@ -1120,7 +1124,7 @@ def get_user( return user def get_headers( - self, content_type: Optional[str] = None + self, content_type: str | None = None ) -> dict[str, str]: if content_type is None: content_type = "application/json" @@ -1154,8 +1158,11 @@ def get_headers( return headers def login( - self, username: str, password: str, create_session: bool = True - ): + self, + username: str, + password: str, + create_session: bool = True, + ) -> None: """Login to server. Args: @@ -1213,13 +1220,13 @@ def login( if create_session: self.create_session() - def logout(self, soft: bool = False): + def logout(self, soft: bool = False) -> None: if self._token_info.token: if not soft: self._logout() self.reset_token() - def raw_post(self, entrypoint: str, **kwargs): + def raw_post(self, entrypoint: str, **kwargs) -> RestApiResponse: url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [POST] {url}") return self._do_rest_request( @@ -1228,7 +1235,7 @@ def raw_post(self, entrypoint: str, **kwargs): **kwargs ) - def raw_put(self, entrypoint: str, **kwargs): + def raw_put(self, entrypoint: str, **kwargs) -> RestApiResponse: url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [PUT] {url}") return self._do_rest_request( @@ -1237,7 +1244,7 @@ def raw_put(self, entrypoint: str, **kwargs): **kwargs ) - def raw_patch(self, entrypoint: str, **kwargs): + def raw_patch(self, entrypoint: str, **kwargs) -> RestApiResponse: url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [PATCH] {url}") return self._do_rest_request( @@ -1246,7 +1253,7 @@ def raw_patch(self, entrypoint: str, **kwargs): **kwargs ) - def raw_get(self, entrypoint: str, **kwargs): + def raw_get(self, entrypoint: str, **kwargs) -> RestApiResponse: url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [GET] {url}") return self._do_rest_request( @@ -1255,7 +1262,7 @@ def raw_get(self, entrypoint: str, **kwargs): **kwargs ) - def raw_delete(self, entrypoint: str, **kwargs): + def raw_delete(self, entrypoint: str, **kwargs) -> RestApiResponse: url = self._endpoint_to_url(entrypoint) self.log.debug(f"Executing [DELETE] {url}") return self._do_rest_request( @@ -1264,22 +1271,22 @@ def raw_delete(self, entrypoint: str, **kwargs): **kwargs ) - def post(self, entrypoint: str, **kwargs): + def post(self, entrypoint: str, **kwargs) -> RestApiResponse: return self.raw_post(entrypoint, json=kwargs) - def put(self, entrypoint: str, **kwargs): + def put(self, entrypoint: str, **kwargs) -> RestApiResponse: return self.raw_put(entrypoint, json=kwargs) - def patch(self, entrypoint: str, **kwargs): + def patch(self, entrypoint: str, **kwargs) -> RestApiResponse: return self.raw_patch(entrypoint, json=kwargs) - def get(self, entrypoint: str, **kwargs): + def get(self, entrypoint: str, **kwargs) -> RestApiResponse: return self.raw_get(entrypoint, params=kwargs) - def delete(self, entrypoint: str, **kwargs): + def delete(self, entrypoint: str, **kwargs) -> RestApiResponse: return self.raw_delete(entrypoint, params=kwargs) - def get_server_config(self): + def get_server_config(self) -> dict[str, Any]: response = self.get("config") response.raise_for_status() return response.data @@ -1306,12 +1313,12 @@ def set_server_config( response = self.post("config", **body) response.raise_for_status() - def get_server_config_overrides(self): + def get_server_config_overrides(self) -> dict[str, Any]: response = self.get("config/overrides") response.raise_for_status() return response.data - def get_server_config_value(self, key: str): + def get_server_config_value(self, key: str) -> Any: response = self.get(f"config/value/{key}") response.raise_for_status() return response.data @@ -1321,8 +1328,8 @@ def download_server_config_file( file_type: Literal["login_background", "studio_logo"], filepath: str, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download server config file. @@ -1350,8 +1357,8 @@ def download_server_config_file_to_stream( file_type: Literal["login_background", "studio_logo"], stream: StreamType, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download server config file to byte stream. @@ -1460,7 +1467,7 @@ def _endpoint_to_url( Args: endpoint (str): Endpoint to be cleaned. - use_rest (Optional[bool]): Use only base server url if set to + use_rest (bool): Use only base server url if set to False, otherwise REST endpoint is used. Returns: @@ -1473,7 +1480,7 @@ def _endpoint_to_url( base_url = self._rest_url if use_rest else self._base_url return f"{base_url}/{endpoint}" - def _logout(self): + def _logout(self) -> None: if self._token_info.is_valid: logout_from_server(self._base_url, self._token_info.token) @@ -1486,7 +1493,7 @@ def _get_server_info(self) -> dict[str, Any]: response.raise_for_status() return response.data - def _get_user_info(self) -> Optional[dict[str, Any]]: + def _get_user_info(self) -> dict[str, Any] | None: if ( self._token_info.token is None or self._token_info.is_valid is False @@ -1510,7 +1517,7 @@ def _do_rest_request( *, handle_invalid_token: bool = True, **kwargs - ): + ) -> RestApiResponse: kwargs.setdefault("timeout", self.timeout) max_retries = kwargs.get("max_retries", self.max_retries) if max_retries < 1: @@ -1637,7 +1644,7 @@ def _download_file_to_stream( stream: StreamType, chunk_size: int, progress: TransferProgress, - ): + ) -> None: headers = self.get_headers() kwargs = { "stream": True, @@ -1705,8 +1712,8 @@ def download_file_to_stream( self, endpoint: str, stream: StreamType, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download file from AYON server to IOStream. @@ -1724,9 +1731,9 @@ def download_file_to_stream( endpoint (str): Endpoint or URL to file that should be downloaded. stream (StreamType): Stream where output will be stored. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. """ @@ -1755,8 +1762,8 @@ def download_file( self, endpoint: str, filepath: str, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download file from AYON server. @@ -1773,9 +1780,9 @@ def download_file( Args: endpoint (str): Endpoint or URL to file that should be downloaded. filepath (str): Path where file will be downloaded. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. """ @@ -1926,8 +1933,8 @@ def download_project_file( file_id: str, filepath: str, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download project file to filepath. @@ -1939,9 +1946,9 @@ def download_project_file( project_name (str): Project name. file_id (str): File id. filepath (str): Path where file will be downloaded. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -1961,8 +1968,8 @@ def download_project_file_to_stream( file_id: str, stream: StreamType, *, - chunk_size: Optional[int] = None, - progress: Optional[TransferProgress] = None, + chunk_size: int | None = None, + progress: TransferProgress | None = None, ) -> TransferProgress: """Download project file to a stream. @@ -1974,9 +1981,9 @@ def download_project_file_to_stream( project_name (str): Project name. file_id (str): File id. stream (StreamType): Stream where output will be stored. - chunk_size (Optional[int]): Size of chunks that are received + chunk_size (int | None): Size of chunks that are received in single loop. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track download progress. Returns: @@ -2025,11 +2032,11 @@ def _upload_file( endpoint: str, stream: StreamType, progress: TransferProgress, - request_type: Optional[RequestType] = None, - chunk_size: Optional[int] = None, + request_type: RequestType | None = None, + chunk_size: int | None = None, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, + content_type: str | None = None, + filename: str | None = None, **kwargs ) -> requests.Response: """Upload file to server. @@ -2039,9 +2046,9 @@ def _upload_file( stream (StreamType): File stream. progress (TransferProgress): Object that gives ability to track progress. - request_type (Optional[RequestType]): Type of request that will + request_type (RequestType | None): Type of request that will be used. Default is PUT. - chunk_size (Optional[int]): Size of chunks that are uploaded + chunk_size (int | None): Size of chunks that are uploaded at once. **kwargs (Any): Additional arguments that will be passed to request function. @@ -2138,11 +2145,11 @@ def upload_file_from_stream( self, endpoint: str, stream: StreamType, - progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, + progress: TransferProgress | None = None, + request_type: RequestType | None = None, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, + content_type: str | None = None, + filename: str | None = None, **kwargs ) -> requests.Response: """Upload file to server from bytes. @@ -2154,12 +2161,12 @@ def upload_file_from_stream( Args: endpoint (str): Endpoint or url where file will be uploaded. stream (StreamType): File content stream. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track upload progress. - request_type (Optional[RequestType]): Type of request that will + request_type (RequestType | None): Type of request that will be used to upload file. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): Filename of file on server. + content_type (str | None): MIME type of the file. + filename (str | None): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -2196,11 +2203,11 @@ def upload_file( self, endpoint: str, filepath: str, - progress: Optional[TransferProgress] = None, - request_type: Optional[RequestType] = None, + progress: TransferProgress | None = None, + request_type: RequestType | None = None, *, - content_type: Optional[str] = None, - filename: Optional[str] = None, + content_type: str | None = None, + filename: str | None = None, **kwargs ) -> requests.Response: """Upload file to server. @@ -2212,12 +2219,12 @@ def upload_file( Args: endpoint (str): Endpoint or url where file will be uploaded. filepath (str): Source filepath. - progress (Optional[TransferProgress]): Object that gives ability + progress (TransferProgress | None): Object that gives ability to track upload progress. - request_type (Optional[RequestType]): Type of request that will + request_type (RequestType | None): Type of request that will be used to upload file. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): Filename of file on server. + content_type (str | None): MIME type of the file. + filename (str | None): Filename of file on server. **kwargs (Any): Additional arguments that will be passed to request function. @@ -2246,10 +2253,10 @@ def upload_reviewable( project_name: str, version_id: str, filepath: str, - label: Optional[str] = None, - content_type: Optional[str] = None, - filename: Optional[str] = None, - progress: Optional[TransferProgress] = None, + label: str | None = None, + content_type: str | None = None, + filename: str | None = None, + progress: TransferProgress | None = None, **kwargs ) -> requests.Response: """Upload reviewable file to server. @@ -2258,12 +2265,12 @@ def upload_reviewable( project_name (str): Project name. version_id (str): Version id. filepath (str): Reviewable file path to upload. - label (Optional[str]): Reviewable label. Filled automatically + label (str | None): Reviewable label. Filled automatically server side with filename. - content_type (Optional[str]): MIME type of the file. - filename (Optional[str]): User as original filename. Filename from + content_type (str | None): MIME type of the file. + filename (str | None): User as original filename. Filename from 'filepath' is used when not filled. - progress (Optional[TransferProgress]): Progress. + progress (TransferProgress | None): Progress. Returns: requests.Response: Server response. @@ -2296,7 +2303,7 @@ def upload_reviewable( **kwargs ) - def trigger_server_restart(self): + def trigger_server_restart(self) -> None: """Trigger server restart. Restart may be required when a change of specific value happened on @@ -2311,13 +2318,13 @@ def trigger_server_restart(self): def query_graphql( self, query: str, - variables: Optional[dict[str, Any]] = None, + variables: dict[str, Any] | None = None, ) -> GraphQlResponse: """Execute GraphQl query. Args: query (str): GraphQl query string. - variables (Optional[dict[str, Any]): Variables that can be + variables (dict[str, Any] | None): Variables that can be used in query. Returns: @@ -2336,7 +2343,7 @@ def query_graphql( def get_graphql_schema(self) -> dict[str, Any]: return self.query_graphql(INTROSPECTION_QUERY).data["data"] - def get_server_schema(self) -> Optional[dict[str, Any]]: + def get_server_schema(self) -> dict[str, Any] | None: """Get server schema with info, url paths, components etc. Todos: @@ -2440,7 +2447,7 @@ def get_rest_entity_by_id( project_name: str, entity_type: str, entity_id: str, - ) -> Optional[AnyEntityDict]: + ) -> AnyEntityDict | None: """Get entity using REST on a project by its id. Args: @@ -2450,7 +2457,7 @@ def get_rest_entity_by_id( entity_id (str): Id of entity. Returns: - Optional[AnyEntityDict]: Received entity data. + AnyEntityDict | None: Received entity data. """ if not all((project_name, entity_type, entity_id)): @@ -2481,9 +2488,9 @@ def send_batch_operations( 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 + can_fail (bool): Server will try to process all operations even if one of them fails. - raise_on_fail (Optional[bool]): Raise exception if an operation + raise_on_fail (bool): Raise exception if an operation fails. You can handle failed operations on your own when set to 'False'. @@ -2531,10 +2538,10 @@ def send_background_batch_operations( 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 + can_fail (bool): Server will try to process all operations even if one of them fails. wait (bool): Wait for operations to end. - raise_on_fail (Optional[bool]): Raise exception if an operation + raise_on_fail (bool): Raise exception if an operation fails. You can handle failed operations on your own when set to 'False'. Used when 'wait' is enabled. @@ -2679,7 +2686,10 @@ def _validate_operations_result( ) def _prepare_fields( - self, entity_type: str, fields: set[str], own_attributes: bool = False + self, + entity_type: str, + fields: set[str], + own_attributes: bool = False, ): if not fields: return @@ -2754,8 +2764,8 @@ def _prepare_link_fields(self, fields: set[str]) -> None: fields.add("links.data") def _prepare_advanced_filters( - self, filters: Union[str, dict[str, Any], None] - ) -> Optional[str]: + self, filters: str | dict[str, Any] | None + ) -> str | None: if not filters: return None @@ -2763,7 +2773,7 @@ def _prepare_advanced_filters( return json.dumps(filters) return filters - def _convert_entity_data(self, entity: AnyEntityDict): + def _convert_entity_data(self, entity: AnyEntityDict) -> None: if not entity: return diff --git a/ayon_api/typing.py b/ayon_api/typing.py index e620de085..37bb92f51 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -6,7 +6,6 @@ Any, TypedDict, Union, - Optional, BinaryIO, NotRequired, ) @@ -64,9 +63,9 @@ class IconDefType(TypedDict): type: IconType - name: Optional[str] - color: Optional[str] - icon: Optional[str] + name: str | None + color: str | None + icon: str | None class EventFilterCondition(TypedDict): @@ -97,7 +96,7 @@ class EventFilter(TypedDict): class BackgroundOperationTask(TypedDict): id: str status: Literal["pending", "in_progress", "completed"] - result: Optional[dict[str, Any]] + result: dict[str, Any] | None AttributeScope = Literal[ @@ -133,29 +132,29 @@ class CreateLinkData(TypedDict): class AttributeEnumItemDict(TypedDict): - value: Union[str, int, float, bool] + value: str | int | float | bool label: str - icon: Union[str, None] - color: Union[str, None] + icon: str | None + color: str | None class AttributeSchemaDataDict(TypedDict): type: AttributeType inherit: bool title: str - description: Optional[str] - example: Optional[Any] - default: Optional[Any] - gt: Union[int, float, None] - lt: Union[int, float, None] - ge: Union[int, float, None] - le: Union[int, float, None] - minLength: Optional[int] - maxLength: Optional[int] - minItems: Optional[int] - maxItems: Optional[int] - regex: Optional[str] - enum: Optional[list[AttributeEnumItemDict]] + description: str | None + example: Any | None + default: Any | None + gt: int | float | None + lt: int | float | None + ge: int | float | None + le: int | float | None + minLength: int | None + maxLength: int | None + minItems: int | None + maxItems: int | None + regex: str | None + enum: list[AttributeEnumItemDict] | None class AttributeSchemaDict(TypedDict): @@ -238,7 +237,7 @@ class BundleInfoDict(TypedDict): isStaging: bool isArchived: bool isDev: bool - activeUser: Optional[str] + activeUser: str | None class BundlesInfoDict(TypedDict): @@ -373,10 +372,10 @@ class NewFolderDict(TypedDict): id: str name: str folderType: str - parentId: Optional[str] + parentId: str | None data: dict[str, Any] attrib: dict[str, Any] - thumbnailId: Optional[str] + thumbnailId: str | None status: NotRequired[str] tags: NotRequired[list[str]] @@ -461,11 +460,11 @@ class EnrollEventData(TypedDict): class FlatFolderDict(TypedDict): id: str - parentId: Optional[str] + parentId: str | None path: str parents: list[str] name: str - label: Optional[str] + label: str | None folderType: str hasTasks: bool hasChildren: bool @@ -485,7 +484,7 @@ class ProjectHierarchyItemDict(TypedDict): hasTasks: bool taskNames: list[str] parents: list[str] - parentId: Optional[str] + parentId: str | None children: list["ProjectHierarchyItemDict"] @@ -495,8 +494,8 @@ class ProjectHierarchyDict(TypedDict): class ProductTypeDict(TypedDict): name: str - color: Optional[str] - icon: Optional[str] + color: str | None + icon: str | None ActionEntityTypes = Literal[ @@ -514,10 +513,10 @@ class ProductTypeDict(TypedDict): class ActionManifestDict(TypedDict): identifier: str label: str - groupLabel: Optional[str] + groupLabel: str | None category: str order: int - icon: Optional[IconDefType] + icon: IconDefType | None adminOnly: bool managerOnly: bool configFields: list[dict[str, Any]] @@ -583,8 +582,8 @@ class ActionFormPayload(BaseActionPayload): class ActionTriggerResponse(TypedDict): type: ActionResponseType success: bool - message: Optional[str] - payload: Optional[ActionPayload] + message: str | None + payload: ActionPayload | None class ActionTakeResponse(TypedDict): @@ -607,7 +606,7 @@ class ActionConfigResponse(TypedDict): value: dict[str, Any] -StreamType = Union[io.BytesIO, BinaryIO] +StreamType = io.BytesIO | BinaryIO class EntityListAttributeDefinitionDict(TypedDict): @@ -631,5 +630,5 @@ class AdvancedFilterConditionDict(TypedDict): class AdvancedFilterDict(TypedDict): - conditions: list[Union[AdvancedFilterConditionDict, "AdvancedFilterDict"]] + conditions: list[AdvancedFilterConditionDict | "AdvancedFilterDict"] operator: AdvancedFilterOperator From 555c8d9d76c4d4cc6146fe5934bee114b41df97e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:12:15 +0200 Subject: [PATCH 496/506] fix positional arguments handling in public api --- ayon_api/_api.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 2bf8f2025..b0c6e02dd 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -2982,8 +2982,8 @@ def get_addon_endpoint( """ con = get_server_api_connection() return con.get_addon_endpoint( - addon_name=addon_name, - addon_version=addon_version, + addon_name, + addon_version, *subpaths, ) @@ -3032,8 +3032,8 @@ def get_addon_url( """ con = get_server_api_connection() return con.get_addon_url( - addon_name=addon_name, - addon_version=addon_version, + addon_name, + addon_version, *subpaths, use_rest=use_rest, ) From 193ecac9b508d9f8f5f38f00449aead8b4ac15da Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 24 Jun 2026 18:16:51 +0200 Subject: [PATCH 497/506] fix how positional arguments are handled --- automated_api.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/automated_api.py b/automated_api.py index ab6ced2d9..b2536606f 100644 --- a/automated_api.py +++ b/automated_api.py @@ -269,7 +269,10 @@ def sig_params_to_str(sig, param_names, api_globals, indent=0): func_params.append("/") for param_name, param in pos_or_kw: - body_params.append(f"{param_name}={param_name}") + body_par = param_name + if not var_positional: + body_par = f"{param_name}={param_name}" + body_params.append(body_par) func_params.append(_kw_default_to_str(param_name, param, api_globals)) if var_positional: From 8c15bf336269666574212478fe0b9d8d9322dbb9 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 25 Jun 2026 09:32:15 +0000 Subject: [PATCH 498/506] Release version 1.2.22 --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index fa3019b94..1a1d45d90 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.22-dev" +__version__ = "1.2.22" diff --git a/pyproject.toml b/pyproject.toml index 27939bd0c..6443e2074 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.22-dev" +version = "1.2.22" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.22-dev" +version = "1.2.22" description = "AYON Python API" authors = [ "ynput.io " From d773da210a083b36ec70abb83f09f4aa0505aeb1 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Thu, 25 Jun 2026 09:32:36 +0000 Subject: [PATCH 499/506] Bump version to 1.2.23-dev --- ayon_api/version.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ayon_api/version.py b/ayon_api/version.py index 1a1d45d90..4ba0b40ba 100644 --- a/ayon_api/version.py +++ b/ayon_api/version.py @@ -1,2 +1,2 @@ """Package declaring Python API for AYON server.""" -__version__ = "1.2.22" +__version__ = "1.2.23-dev" diff --git a/pyproject.toml b/pyproject.toml index 6443e2074..a4cea5722 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ayon_python_api" -version = "1.2.22" +version = "1.2.23-dev" description = "AYON Python API" license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} @@ -28,7 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "ayon_python_api" -version = "1.2.22" +version = "1.2.23-dev" description = "AYON Python API" authors = [ "ynput.io " From 045fdce481b32a97954fc39312c16e82e008574d Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:49:16 +0200 Subject: [PATCH 500/506] added bulk creation of links --- ayon_api/__init__.py | 2 + ayon_api/_api.py | 37 +++++++++++++++- ayon_api/_api_helpers/links.py | 80 +++++++++++++++++++++++++++++++++- ayon_api/typing.py | 10 ++++- 4 files changed, 124 insertions(+), 5 deletions(-) diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index 2f04c2800..aef55d7e4 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -275,6 +275,7 @@ delete_link_type, make_sure_link_type_exists, create_link, + create_links, delete_link, get_entities_links, get_folders_links, @@ -594,6 +595,7 @@ "delete_link_type", "make_sure_link_type_exists", "create_link", + "create_links", "delete_link", "get_entities_links", "get_folders_links", diff --git a/ayon_api/_api.py b/ayon_api/_api.py index b0c6e02dd..08ed230ff 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -53,6 +53,7 @@ BackgroundOperationTask, LinkDirection, CreateLinkData, + CreateLinkResponseData, EventFilter, EventStatus, EnrollEventData, @@ -7504,7 +7505,7 @@ def create_link( output_type: str, link_name: Optional[str] = None, data: Optional[dict[str, Any]] = None, -) -> CreateLinkData: +) -> CreateLinkResponseData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -7527,7 +7528,7 @@ def create_link( with the link. Returns: - CreateLinkData: Information about link. + CreateLinkResponseData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -7546,6 +7547,38 @@ def create_link( ) +def create_links( + project_name: str, + links: list[dict[str, Any]], +) -> None: + """Create multiple links in a single request. + + Example of link data:: + [ + { + "input": "59a212c0d2e211eda0e20242ac120001", + "output": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference|folder|folder", + "name": "my_link", + "data": {"key": "value"} + } + ] + + Args: + project_name (str): Project where links are created. + links (list[dict[str, Any]]): List of link data. + + Raises: + ValueError: Link data is invalid. + + """ + con = get_server_api_connection() + return con.create_links( + project_name=project_name, + links=links, + ) + + def delete_link( project_name: str, link_id: str, diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index a50b21817..a83e1f44d 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -200,7 +200,7 @@ def create_link( output_type: str, link_name: Optional[str] = None, data: Optional[dict[str, Any]] = None, - ) -> CreateLinkData: + ) -> CreateLinkResponseData: """Create link between 2 entities. Link has a type which must already exists on a project. @@ -223,7 +223,7 @@ def create_link( with the link. Returns: - CreateLinkData: Information about link. + CreateLinkResponseData: Information about link. Raises: HTTPRequestError: Server error happened. @@ -249,6 +249,59 @@ def create_link( response.raise_for_status() return response.data + def create_links( + self, + project_name: str, + links: list[dict[str, Any]], + ) -> None: + """Create multiple links in a single request. + + Example of link data:: + [ + { + "input": "59a212c0d2e211eda0e20242ac120001", + "output": "59a212c0d2e211eda0e20242ac120002", + "linkType": "reference|folder|folder", + "name": "my_link", + "data": {"key": "value"} + } + ] + + Args: + project_name (str): Project where links are created. + links (list[dict[str, Any]]): List of link data. + + Raises: + ValueError: Link data is invalid. + + """ + if not links: + return + + for link in links: + self._validate_link_data(link) + + if self.get_server_version_tuple() < (1, 15, 8): + for link in links: + link_type, in_type, out_type = link["linkType"].split("|") + self.create_link( + project_name, + link_type, + link["input"], + in_type, + link["output"], + out_type, + link_name=link.get("name") or None, + data=link.get("data") or None, + ) + return + + response = self.post( + f"projects/{project_name}/links/bulk", + links=links + ) + response.raise_for_status() + def delete_link(self, project_name: str, link_id: str) -> None: """Remove link by id. @@ -619,6 +672,29 @@ def get_representation_links( project_name, [representation_id], link_types, link_direction )[representation_id] + def _validate_link_data(self, link_data: dict[str, Any]) -> None: + """Validate link data before sending to server. + + Args: + link_data (dict[str, Any]): Link data to validate. + + Raises: + ValueError: Link data is invalid. + + """ + required_keys = {"input", "output", "linkType"} + missing_keys = required_keys - link_data.keys() + if missing_keys: + mk = ", ".join((f"'{key}'" for key in missing_keys)) + raise ValueError(f"Missing required keys in link data {mk}") + + link_type_parts = link_data["linkType"].split("|") + if len(link_type_parts) != 3: + raise ValueError( + f"Invalid linkType format: {link_data['linkType']}. " + "Expected format: 'link_type|input_type|output_type'" + ) + def _prepare_link_filters( self, filters: dict[str, Any], diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 37bb92f51..368d522ca 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -127,10 +127,18 @@ class BackgroundOperationTask(TypedDict): LinkDirection = Literal["in", "out"] -class CreateLinkData(TypedDict): +class CreateLinkResponseData(TypedDict): id: str +class CreateLinkData(TypedDict): + input: str + output: str + linkType: str + data: dict[str, Any] | None = None + name: str | None = None + + class AttributeEnumItemDict(TypedDict): value: str | int | float | bool label: str From 5ba93eef9b07aae66be8fdf5e728c48aff309864 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:50:32 +0200 Subject: [PATCH 501/506] fix typehints of list methods --- ayon_api/_api.py | 8 ++++---- ayon_api/_api_helpers/lists.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index b0c6e02dd..bd007fb2c 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -8003,8 +8003,8 @@ def create_entity_list( *, list_type: Optional[str] = None, access: Optional[dict[str, Any]] = None, - attrib: Optional[list[dict[str, Any]]] = None, - data: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[list[str]] = None, template: Optional[dict[str, Any]] = None, entity_list_folder_id: Optional[str] = None, @@ -8060,8 +8060,8 @@ def update_entity_list( *, label: Optional[str] = None, access: Optional[dict[str, Any]] = None, - attrib: Optional[list[dict[str, Any]]] = None, - data: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[list[str]] = None, entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, owner: Optional[str] = None, diff --git a/ayon_api/_api_helpers/lists.py b/ayon_api/_api_helpers/lists.py index 3d9b6967d..585504fb4 100644 --- a/ayon_api/_api_helpers/lists.py +++ b/ayon_api/_api_helpers/lists.py @@ -157,8 +157,8 @@ def create_entity_list( *, list_type: Optional[str] = None, access: Optional[dict[str, Any]] = None, - attrib: Optional[list[dict[str, Any]]] = None, - data: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[list[str]] = None, template: Optional[dict[str, Any]] = None, entity_list_folder_id: Optional[str] = None, @@ -225,8 +225,8 @@ def update_entity_list( *, label: Optional[str] = None, access: Optional[dict[str, Any]] = None, - attrib: Optional[list[dict[str, Any]]] = None, - data: Optional[list[dict[str, Any]]] = None, + attrib: Optional[dict[str, Any]] = None, + data: Optional[dict[str, Any]] = None, tags: Optional[list[str]] = None, entity_list_folder_id: str | None | type[NOT_SET] = NOT_SET, owner: Optional[str] = None, From a7119826a8a4ef38a7ad083339ec2b7fc0608f95 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:59:20 +0200 Subject: [PATCH 502/506] use 'CreateLinkData' --- ayon_api/_api.py | 4 ++-- ayon_api/_api_helpers/links.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/ayon_api/_api.py b/ayon_api/_api.py index 159a0f03f..dfa178e88 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -7549,7 +7549,7 @@ def create_link( def create_links( project_name: str, - links: list[dict[str, Any]], + links: list[CreateLinkData], ) -> None: """Create multiple links in a single request. @@ -7566,7 +7566,7 @@ def create_links( Args: project_name (str): Project where links are created. - links (list[dict[str, Any]]): List of link data. + links (list[CreateLinkData]): List of link data. Raises: ValueError: Link data is invalid. diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index a83e1f44d..5cdec5d99 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -252,7 +252,7 @@ def create_link( def create_links( self, project_name: str, - links: list[dict[str, Any]], + links: list[CreateLinkData], ) -> None: """Create multiple links in a single request. @@ -269,7 +269,7 @@ def create_links( Args: project_name (str): Project where links are created. - links (list[dict[str, Any]]): List of link data. + links (list[CreateLinkData]): List of link data. Raises: ValueError: Link data is invalid. From 139a3d0b519464899e6c33f31bc71db50eb596f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 10:01:32 +0200 Subject: [PATCH 503/506] add missing import --- ayon_api/_api_helpers/links.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index 5cdec5d99..913ad8ab4 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -15,7 +15,11 @@ from .base import BaseServerAPI if typing.TYPE_CHECKING: - from ayon_api.typing import LinkDirection, CreateLinkData + from ayon_api.typing import ( + LinkDirection, + CreateLinkData, + CreateLinkResponseData, + ) class LinksAPI(BaseServerAPI): From 13aeccb27598fdde004ca466ecd85e3f0dedd2a8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:52:23 +0200 Subject: [PATCH 504/506] use 'NotRequired' --- ayon_api/typing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/typing.py b/ayon_api/typing.py index 368d522ca..00155dcc5 100644 --- a/ayon_api/typing.py +++ b/ayon_api/typing.py @@ -135,8 +135,8 @@ class CreateLinkData(TypedDict): input: str output: str linkType: str - data: dict[str, Any] | None = None - name: str | None = None + data: NotRequired[dict[str, Any] | None] + name: NotRequired[str | None] class AttributeEnumItemDict(TypedDict): From a643361fb5ce83031514782513ae5e436669b59e Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:53:32 +0200 Subject: [PATCH 505/506] better error message --- ayon_api/_api_helpers/links.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index 913ad8ab4..ea42db170 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -689,8 +689,8 @@ def _validate_link_data(self, link_data: dict[str, Any]) -> None: required_keys = {"input", "output", "linkType"} missing_keys = required_keys - link_data.keys() if missing_keys: - mk = ", ".join((f"'{key}'" for key in missing_keys)) - raise ValueError(f"Missing required keys in link data {mk}") + mk = ", ".join(f"'{key}'" for key in sorted(missing_keys)) + raise ValueError(f"Missing required keys in link data: {mk}") link_type_parts = link_data["linkType"].split("|") if len(link_type_parts) != 3: From 9943761dcc02a1f5d1d3074c093ba0a9751ce139 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:54:35 +0200 Subject: [PATCH 506/506] better validation --- ayon_api/_api_helpers/links.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/ayon_api/_api_helpers/links.py b/ayon_api/_api_helpers/links.py index ea42db170..5dd6136fc 100644 --- a/ayon_api/_api_helpers/links.py +++ b/ayon_api/_api_helpers/links.py @@ -692,11 +692,17 @@ def _validate_link_data(self, link_data: dict[str, Any]) -> None: mk = ", ".join(f"'{key}'" for key in sorted(missing_keys)) raise ValueError(f"Missing required keys in link data: {mk}") - link_type_parts = link_data["linkType"].split("|") + link_type = link_data["linkType"] + if not isinstance(link_type, str): + raise ValueError( + f"Invalid linkType type: {type(link_type)}. Expected 'str'" + ) + + link_type_parts = link_type.split("|") if len(link_type_parts) != 3: raise ValueError( - f"Invalid linkType format: {link_data['linkType']}. " - "Expected format: 'link_type|input_type|output_type'" + f"Invalid linkType format: {link_type}. Expected format:" + " 'link_type|input_type|output_type'" ) def _prepare_link_filters(