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 01/20] 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 02/20] 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 03/20] 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 04/20] 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 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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 10/20] 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 11/20] 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 12/20] 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 13/20] 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 14/20] 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 15/20] 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 16/20] 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 17/20] 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 18/20] 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 19/20] 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 20/20] 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,