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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 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 9/9] 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"