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 01/27] :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 02/27] :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 03/27] :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 04/27] :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 05/27] :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 06/27] 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 07/27] 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 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 08/27] 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 09/27] 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 10/27] 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 11/27] 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 12/27] 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 13/27] 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 14/27] 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 15/27] 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 16/27] 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 17/27] 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 18/27] 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 19/27] 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 20/27] 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 21/27] 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 22/27] :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 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 23/27] :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 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 24/27] 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 25/27] 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 26/27] 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 27/27] 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 (