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' diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 93ff2877f..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", @@ -51,14 +77,19 @@ # --- Project --- DEFAULT_PROJECT_FIELDS = { "active", + "library", "name", "code", "config", "createdAt", + "updatedAt", "data", "folderTypes", "taskTypes", - "productTypes", + "linkTypes", + "statuses", + "tags", + "attrib", } # --- Folders --- diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index 18b76f059..aef04730a 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) @@ -119,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 0409e9310..a6d5aa249 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 @@ -40,6 +41,8 @@ SERVER_RETRIES_ENV_KEY, DEFAULT_FOLDER_TYPE_FIELDS, DEFAULT_TASK_TYPE_FIELDS, + DEFAULT_PROJECT_STATUSES_FIELDS, + DEFAULT_PROJECT_TAGS_FIELDS, DEFAULT_PRODUCT_TYPE_FIELDS, DEFAULT_PROJECT_FIELDS, DEFAULT_FOLDER_FIELDS, @@ -55,9 +58,7 @@ ) from .graphql import GraphQlQuery, INTROSPECTION_QUERY from .graphql_queries import ( - project_graphql_query, projects_graphql_query, - project_product_types_query, product_types_query, folders_graphql_query, tasks_graphql_query, @@ -1106,7 +1107,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 @@ -1587,7 +1587,6 @@ def get_events( ) statuses = states - filters = {} if not _prepare_list_filters( filters, @@ -1768,7 +1767,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]]", @@ -2732,6 +2730,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) @@ -2754,12 +2755,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) @@ -4397,18 +4392,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( @@ -4586,22 +4570,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. - - """ - if fields is None: - return True - for field in fields: - if field.startswith("config"): - return True - return False - def get_projects( self, active: "Union[bool, None]" = True, @@ -4628,26 +4596,26 @@ def get_projects( if fields is not None: 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 - return - - self._prepare_fields("project", fields, own_attributes) - if active is not None: - fields.add("active") + 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} - 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 + 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, @@ -4672,27 +4640,132 @@ def get_project( if fields is not None: fields = set(fields) - 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 + 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 - self._prepare_fields("project", fields, own_attributes) + def _get_project_graphql_fields( + self, fields: Optional[Set[str]] + ) -> Tuple[Set[str], bool]: + """Fetch of project must be done using REST endpoint. - query = project_graphql_query(fields) - query.set_variable_value("projectName", project_name) + Returns: + set[str]: GraphQl fields. - parsed_data = query.query(self) + """ + 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, + } - project = parsed_data["project"] - if project is not None: - project["name"] = project_name - if own_attributes: - fill_own_attribs(project) + # 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") - return project + 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, @@ -6005,12 +6078,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. @@ -6018,22 +6091,29 @@ 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' with 'productTypes' in 'fields' 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, 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,7 +6129,15 @@ def get_product_type_names( set[str]: Product type names. """ - if project_name and product_ids: + 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, @@ -6063,9 +6151,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( @@ -8928,29 +9014,48 @@ 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 "taskTypes" in fields: - fields.remove("taskTypes") - fields |= { - f"taskTypes.{name}" - for name in self.get_default_fields_for_type("taskType") - } + if entity_type != "project": + return - if "productTypes" in fields: - fields.remove("productTypes") - fields |= { - f"productTypes.{name}" - for name in self.get_default_fields_for_type( - "productType" - ) - } + # Use 'data' to fill 'bundle' data + if "bundle" in 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) + 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) + 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} + + 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") + 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: