diff --git a/ayon_api/__init__.py b/ayon_api/__init__.py index ca8721284..8d3ef554c 100644 --- a/ayon_api/__init__.py +++ b/ayon_api/__init__.py @@ -54,6 +54,7 @@ get_info, get_server_version, get_server_version_tuple, + is_product_base_type_supported, get_users, get_user_by_name, get_user, @@ -332,6 +333,7 @@ "get_info", "get_server_version", "get_server_version_tuple", + "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 d6dce707c..c13198cbb 100644 --- a/ayon_api/_api.py +++ b/ayon_api/_api.py @@ -721,6 +721,13 @@ def get_server_version_tuple() -> ServerVersion: return con.get_server_version_tuple() +def is_product_base_type_supported() -> bool: + """Product base types are available on server. + """ + con = get_server_api_connection() + return con.is_product_base_type_supported() + + def get_users( project_name: Optional[str] = None, usernames: Optional[Iterable[str]] = None, @@ -4900,6 +4907,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, @@ -4952,6 +4960,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, @@ -5110,6 +5119,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. @@ -5124,6 +5134,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. @@ -5142,6 +5153,7 @@ def create_product( tags=tags, status=status, active=active, + product_base_type=product_base_type, product_id=product_id, ) @@ -5152,6 +5164,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, @@ -5171,6 +5184,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. @@ -5185,6 +5199,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/base.py b/ayon_api/_api_helpers/base.py index d7733ce64..bf209a0a1 100644 --- a/ayon_api/_api_helpers/base.py +++ b/ayon_api/_api_helpers/base.py @@ -25,6 +25,9 @@ class BaseServerAPI: def log(self) -> logging.Logger: raise NotImplementedError() + def is_product_base_type_supported(self) -> bool: + raise NotImplementedError() + def get_server_version(self) -> str: raise NotImplementedError() diff --git a/ayon_api/_api_helpers/products.py b/ayon_api/_api_helpers/products.py index 13fdb9b80..353d36005 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, @@ -32,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, @@ -59,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. @@ -83,6 +87,11 @@ def get_products( if not project_name: return + if product_base_types and not self.is_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: @@ -150,6 +159,7 @@ def get_products( filters, ("productIds", product_ids), ("productTypes", product_types), + ("productBaseTypes", product_base_types), ("productStatuses", statuses), ("productTags", tags), ): @@ -378,6 +388,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 +403,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 +411,14 @@ def create_product( str: Product id. """ + if ( + product_base_type is not None + and not self.is_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 +428,7 @@ def create_product( "folderId": folder_id, } for key, value in ( + ("productBaseType", product_base_type), ("attrib", attrib), ("data", data), ("tags", tags), @@ -431,6 +452,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 +472,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 +480,19 @@ def update_product( active (Optional[bool]): New product active state. """ + if ( + product_base_type is not None + and not self.is_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), diff --git a/ayon_api/_api_helpers/projects.py b/ayon_api/_api_helpers/projects.py index 970abfc3b..cda8bb399 100644 --- a/ayon_api/_api_helpers/projects.py +++ b/ayon_api/_api_helpers/projects.py @@ -7,7 +7,11 @@ 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, + DEFAULT_PRODUCT_TYPE_FIELDS, +) from ayon_api.utils import prepare_query_string, fill_own_attribs from ayon_api.graphql_queries import projects_graphql_query @@ -679,9 +683,8 @@ def _get_project_graphql_fields( 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") + for f_name in DEFAULT_PRODUCT_TYPE_FIELDS: + fields.add(f"{field}.{f_name}") elif field.startswith("productTypes"): must_use_graphql = True @@ -690,7 +693,8 @@ def _get_project_graphql_fields( elif field == "productBaseTypes": must_use_graphql = True fields.discard(field) - graphql_fields.add("productBaseTypes.name") + for f_name in DEFAULT_PRODUCT_BASE_TYPE_FIELDS: + fields.add(f"{field}.{f_name}") elif field.startswith("productBaseTypes"): must_use_graphql = True diff --git a/ayon_api/constants.py b/ayon_api/constants.py index 1bfd14c99..21d75e324 100644 --- a/ayon_api/constants.py +++ b/ayon_api/constants.py @@ -82,6 +82,13 @@ "color", } +# --- Product base type --- +DEFAULT_PRODUCT_BASE_TYPE_FIELDS = { + # Ignore 'icon' and 'color' + # - current server implementation always returns 'null' + "name", +} + # --- Project --- DEFAULT_PROJECT_FIELDS = { "active", diff --git a/ayon_api/entity_hub.py b/ayon_api/entity_hub.py index f12c00776..c77b9bf68 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 is_product_base_type_supported(self) -> bool: + return self._connection.is_product_base_type_supported() + def get_attributes_for_type( self, entity_type: EntityType ) -> dict[str, AttributeSchemaDict]: @@ -494,6 +497,7 @@ def add_new_product( self, name: str, product_type: str, + product_base_type: Optional[str] = None, folder_id: Optional[str] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = None, @@ -502,10 +506,11 @@ def add_new_product( entity_id: Optional[str] = None, created: Optional[bool] = True, ) -> ProductEntity: - """Create task object and add it to entity hub. + """Create a product 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 (Optional[str]): Parent folder id. tags (Optional[Iterable[str]]): Folder tags. @@ -517,6 +522,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. @@ -524,6 +534,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, @@ -3534,6 +3545,7 @@ def __init__( self, name: str, product_type: str, + product_base_type: Optional[str] = None, folder_id: Union[str, None, _CustomNone] = UNKNOWN_VALUE, tags: Optional[Iterable[str]] = None, attribs: Optional[dict[str, Any]] = None, @@ -3555,8 +3567,10 @@ def __init__( entity_hub=entity_hub, ) self._product_type = product_type + 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 @@ -3574,9 +3588,25 @@ def set_product_type(self, product_type: str) -> None: 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) -> 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]: @@ -3588,6 +3618,12 @@ def changes(self) -> dict[str, Any]: if self._orig_product_type != self._product_type: changes["productType"] = self._product_type + if ( + self._entity_hub.is_product_base_type_supported() + and self._orig_product_base_type != self._product_base_type + ): + changes["productBaseType"] = self._product_base_type + return changes @classmethod @@ -3597,6 +3633,7 @@ def from_entity_data( return cls( name=product["name"], product_type=product["productType"], + product_base_type=product.get("productBaseType"), folder_id=product["folderId"], tags=product["tags"], attribs=product["attrib"], @@ -3617,6 +3654,12 @@ def to_create_body_data(self) -> dict[str, Any]: "folderId": self.parent_id, } + if ( + self._entity_hub.is_product_base_type_supported() + and self.product_base_type + ): + output["productBaseType"] = self.product_base_type + attrib = self.attribs.to_dict() if attrib: output["attrib"] = attrib diff --git a/ayon_api/graphql_queries.py b/ayon_api/graphql_queries.py index be5e39673..815cc9a77 100644 --- a/ayon_api/graphql_queries.py +++ b/ayon_api/graphql_queries.py @@ -278,6 +278,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_name_regex_var = query.add_variable("productNameRegex", "String!") product_path_regex_var = query.add_variable("productPathRegex", "String!") statuses_var = query.add_variable("productStatuses", "[String!]") @@ -291,6 +293,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 8b91340ca..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: @@ -200,13 +201,14 @@ def new_product_entity( tags: Optional[list[str]] = None, attribs: Optional[dict[str, Any]] = None, data: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None, entity_id: Optional[str] = None, ) -> NewProductDict: - """Create skeleton data of product entity. + """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_type (str): Product type. folder_id (str): Parent folder id. status (Optional[str]): Product status. @@ -217,6 +219,7 @@ def new_product_entity( is used if not passed. entity_id (Optional[str]): Predefined id of entity. New id is created if not passed. + product_base_type (str): Base type of the product, e.g. "render". Returns: NewProductDict: Skeleton of product entity. @@ -236,6 +239,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: @@ -1231,6 +1237,7 @@ def create_product( tags: Optional[list[str]] = None, status: Optional[str] = None, active: Optional[bool] = None, + product_base_type: Optional[str] = None, product_id: Optional[str] = None, ) -> CreateOperation: """Create new product. @@ -1238,13 +1245,13 @@ def create_product( Args: project_name (str): Project name. name (str): Product name. - product_type (str): Product type. folder_id (str): Parent folder id. attrib (Optional[dict[str, Any]]): Product attributes. data (Optional[dict[str, Any]]): Product data. tags (Optional[Iterable[str]]): Product tags. status (Optional[str]): Product status. active (Optional[bool]): Product active state. + product_base_type (Optional[str]): Product base type. product_id (Optional[str]): Product id. If not passed new id is generated. @@ -1260,12 +1267,22 @@ def create_product( "productType": product_type, "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), ("tags", tags), ("status", status), ("active", active), + ("productBaseType", product_base_type) ): if value is not None: create_data[key] = value @@ -1281,6 +1298,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[list[str]] = None, @@ -1289,7 +1307,8 @@ def update_product( ) -> 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 product entity. Update of ``attrib`` does change only passed attributes. If you want to unset value, use ``None``. @@ -1300,6 +1319,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. @@ -1310,20 +1330,29 @@ 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 ( + product_base_type + and not self._con.is_product_base_type_supported() ): - if value is not None: - update_data[key] = value + raise UnsupportedServerVersion( + "Product base type is not supported for your server version." + ) + 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", @@ -1336,7 +1365,7 @@ def delete_product( project_name: str, product_id: str, ) -> DeleteOperation: - """Delete product. + """Delete a product. Args: project_name (str): Project name. diff --git a/ayon_api/server_api.py b/ayon_api/server_api.py index 87bb044d5..292cdfdfe 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,15 @@ def graphql_allows_traits_in_representations(self) -> bool: ) return self._graphql_allows_traits_in_representations + 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 + 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]]: if self._access_token is None: return None @@ -1805,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)