diff --git a/client/ayon_core/tools/loader/abstract.py b/client/ayon_core/tools/loader/abstract.py index d0d7cd430b..741eb59f81 100644 --- a/client/ayon_core/tools/loader/abstract.py +++ b/client/ayon_core/tools/loader/abstract.py @@ -1,5 +1,6 @@ +from __future__ import annotations from abc import ABC, abstractmethod -from typing import List +from typing import List, Optional, TypedDict from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -8,15 +9,62 @@ from ayon_core.lib.attribute_definitions import ( ) +IconData = TypedDict("IconData", { + "type": str, + "name": str, + "color": str +}) + +ProductBaseTypeItemData = TypedDict("ProductBaseTypeItemData", { + "name": str, + "icon": IconData +}) + + +VersionItemData = TypedDict("VersionItemData", { + "version_id": str, + "version": int, + "is_hero": bool, + "product_id": str, + "task_id": Optional[str], + "thumbnail_id": Optional[str], + "published_time": Optional[str], + "author": Optional[str], + "status": Optional[str], + "frame_range": Optional[str], + "duration": Optional[int], + "handles": Optional[str], + "step": Optional[int], + "comment": Optional[str], + "source": Optional[str] +}) + + +ProductItemData = TypedDict("ProductItemData", { + "product_id": str, + "product_type": str, + "product_base_type": str, + "product_name": str, + "product_icon": IconData, + "product_type_icon": IconData, + "product_base_type_icon": IconData, + "group_name": str, + "folder_id": str, + "folder_label": str, + "version_items": dict[str, VersionItemData], + "product_in_scene": bool +}) + + class ProductTypeItem: """Item representing product type. Args: name (str): Product type name. - icon (dict[str, Any]): Product type icon definition. + icon (IconData): Product type icon definition. """ - def __init__(self, name, icon): + def __init__(self, name: str, icon: IconData): self.name = name self.icon = icon @@ -31,6 +79,24 @@ class ProductTypeItem: return cls(**data) +class ProductBaseTypeItem: + """Item representing product base type.""" + + def __init__(self, name: str, icon: IconData): + self.name = name + self.icon = icon + + def to_data(self) -> ProductBaseTypeItemData: + return { + "name": self.name, + "icon": self.icon, + } + + @classmethod + def from_data(cls, data: ProductBaseTypeItemData): + return cls(**data) + + class ProductItem: """Product item with it versions. @@ -38,8 +104,8 @@ class ProductItem: product_id (str): Product id. product_type (str): Product type. product_name (str): Product name. - product_icon (dict[str, Any]): Product icon definition. - product_type_icon (dict[str, Any]): Product type icon definition. + product_icon (IconData): Product icon definition. + product_type_icon (IconData): Product type icon definition. product_in_scene (bool): Is product in scene (only when used in DCC). group_name (str): Group name. folder_id (str): Folder id. @@ -49,35 +115,42 @@ class ProductItem: def __init__( self, - product_id, - product_type, - product_name, - product_icon, - product_type_icon, - product_in_scene, - group_name, - folder_id, - folder_label, - version_items, + product_id: str, + product_type: str, + product_base_type: str, + product_name: str, + product_icon: IconData, + product_type_icon: IconData, + product_base_type_icon: IconData, + group_name: str, + folder_id: str, + folder_label: str, + version_items: dict[str, VersionItem], + *, + product_in_scene: bool, ): self.product_id = product_id self.product_type = product_type + self.product_base_type = product_base_type self.product_name = product_name self.product_icon = product_icon self.product_type_icon = product_type_icon + self.product_base_type_icon = product_base_type_icon self.product_in_scene = product_in_scene self.group_name = group_name self.folder_id = folder_id self.folder_label = folder_label self.version_items = version_items - def to_data(self): + def to_data(self) -> ProductItemData: return { "product_id": self.product_id, "product_type": self.product_type, + "product_base_type": self.product_base_type, "product_name": self.product_name, "product_icon": self.product_icon, "product_type_icon": self.product_type_icon, + "product_base_type_icon": self.product_base_type_icon, "product_in_scene": self.product_in_scene, "group_name": self.group_name, "folder_id": self.folder_id, @@ -124,21 +197,22 @@ class VersionItem: def __init__( self, - version_id, - version, - is_hero, - product_id, - task_id, - thumbnail_id, - published_time, - author, - status, - frame_range, - duration, - handles, - step, - comment, - source, + *, + version_id: str, + version: int, + is_hero: bool, + product_id: str, + task_id: Optional[str] = None, + thumbnail_id: Optional[str] = None, + published_time: Optional[str] = None, + author: Optional[str] = None, + status: Optional[str] = None, + frame_range: Optional[str] = None, + duration: Optional[int] = None, + handles: Optional[str] = None, + step: Optional[int] = None, + comment: Optional[str] = None, + source: Optional[str] = None, ): self.version_id = version_id self.product_id = product_id @@ -198,7 +272,7 @@ class VersionItem: def __le__(self, other): return self.__eq__(other) or self.__lt__(other) - def to_data(self): + def to_data(self) -> VersionItemData: return { "version_id": self.version_id, "product_id": self.product_id, @@ -218,7 +292,7 @@ class VersionItem: } @classmethod - def from_data(cls, data): + def from_data(cls, data: VersionItemData): return cls(**data) diff --git a/client/ayon_core/tools/loader/models/products.py b/client/ayon_core/tools/loader/models/products.py index 34acc0550c..da2b049f50 100644 --- a/client/ayon_core/tools/loader/models/products.py +++ b/client/ayon_core/tools/loader/models/products.py @@ -1,19 +1,29 @@ +"""Products model for loader tools.""" +from __future__ import annotations import collections import contextlib +from typing import TYPE_CHECKING, Iterable, Optional import arrow import ayon_api from ayon_api.operations import OperationsSession + from ayon_core.lib import NestedCacheItem from ayon_core.style import get_default_entity_icon_color from ayon_core.tools.loader.abstract import ( + IconData, ProductTypeItem, + ProductBaseTypeItem, ProductItem, VersionItem, RepreItem, ) +if TYPE_CHECKING: + from ayon_api.typing import ProductBaseTypeDict, ProductDict, VersionDict + + PRODUCTS_MODEL_SENDER = "products.model" @@ -70,9 +80,10 @@ def version_item_from_entity(version): def product_item_from_entity( - product_entity, + product_entity: ProductDict, version_entities, - product_type_items_by_name, + product_type_items_by_name: dict[str, ProductTypeItem], + product_base_type_items_by_name: dict[str, ProductBaseTypeItem], folder_label, product_in_scene, ): @@ -88,9 +99,21 @@ def product_item_from_entity( # Cache the item for future use product_type_items_by_name[product_type] = product_type_item - product_type_icon = product_type_item.icon + product_base_type = product_entity.get("productBaseType") + product_base_type_item = product_base_type_items_by_name.get( + product_base_type) + # Same as for product type item above. Not sure if this is still needed + # though. + if product_base_type_item is None: + product_base_type_item = create_default_product_base_type_item( + product_base_type) + # Cache the item for future use + product_base_type_items_by_name[product_base_type] = ( + product_base_type_item) - product_icon = { + product_type_icon = product_type_item.icon + product_base_type_icon = product_base_type_item.icon + product_icon: IconData = { "type": "awesome-font", "name": "fa.file-o", "color": get_default_entity_icon_color(), @@ -103,9 +126,11 @@ def product_item_from_entity( return ProductItem( product_id=product_entity["id"], product_type=product_type, + product_base_type=product_base_type, product_name=product_entity["name"], product_icon=product_icon, product_type_icon=product_type_icon, + product_base_type_icon=product_base_type_icon, product_in_scene=product_in_scene, group_name=group, folder_id=product_entity["folderId"], @@ -114,11 +139,12 @@ def product_item_from_entity( ) -def product_type_item_from_data(product_type_data): +def product_type_item_from_data( + product_type_data: ProductDict) -> ProductTypeItem: # TODO implement icon implementation # icon = product_type_data["icon"] # color = product_type_data["color"] - icon = { + icon: IconData = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -127,8 +153,30 @@ def product_type_item_from_data(product_type_data): return ProductTypeItem(product_type_data["name"], icon) -def create_default_product_type_item(product_type): - icon = { +def product_base_type_item_from_data( + product_base_type_data: ProductBaseTypeDict +) -> ProductBaseTypeItem: + """Create product base type item from data. + + Args: + product_base_type_data (ProductBaseTypeDict): Product base type data. + + Returns: + ProductBaseTypeDict: Product base type item. + + """ + icon: IconData = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductBaseTypeItem( + name=product_base_type_data["name"], + icon=icon) + + +def create_default_product_type_item(product_type: str) -> ProductTypeItem: + icon: IconData = { "type": "awesome-font", "name": "fa.folder", "color": "#0091B2", @@ -136,10 +184,28 @@ def create_default_product_type_item(product_type): return ProductTypeItem(product_type, icon) +def create_default_product_base_type_item( + product_base_type: str) -> ProductBaseTypeItem: + """Create default product base type item. + + Args: + product_base_type (str): Product base type name. + + Returns: + ProductBaseTypeItem: Default product base type item. + """ + icon: IconData = { + "type": "awesome-font", + "name": "fa.folder", + "color": "#0091B2", + } + return ProductBaseTypeItem(product_base_type, icon) + + class ProductsModel: """Model for products, version and representation. - All of the entities are product based. This model prepares data for UI + All the entities are product based. This model prepares data for UI and caches it for faster access. Note: @@ -161,6 +227,8 @@ class ProductsModel: # Cache helpers self._product_type_items_cache = NestedCacheItem( levels=1, default_factory=list, lifetime=self.lifetime) + self._product_base_type_items_cache = NestedCacheItem( + levels=1, default_factory=list, lifetime=self.lifetime) self._product_items_cache = NestedCacheItem( levels=2, default_factory=dict, lifetime=self.lifetime) self._repre_items_cache = NestedCacheItem( @@ -199,6 +267,31 @@ class ProductsModel: ]) return cache.get_data() + def get_product_base_type_items( + self, + project_name: Optional[str]) -> list[ProductBaseTypeItem]: + """Product base type items for project. + + Args: + project_name (optional, str): Project name. + + Returns: + list[ProductBaseTypeDict]: Product base type items. + + """ + if not project_name: + return [] + + cache = self._product_base_type_items_cache[project_name] + if not cache.is_valid: + product_base_types = ayon_api.get_project_product_base_types( + project_name) + cache.update_data([ + product_base_type_item_from_data(product_base_type) + for product_base_type in product_base_types + ]) + return cache.get_data() + def get_product_items(self, project_name, folder_ids, sender): """Product items with versions for project and folder ids. @@ -449,11 +542,12 @@ class ProductsModel: def _create_product_items( self, - project_name, - products, - versions, + project_name: str, + products: Iterable[ProductDict], + versions: Iterable[VersionDict], folder_items=None, product_type_items=None, + product_base_type_items: Optional[Iterable[ProductBaseTypeItem]] = None ): if folder_items is None: folder_items = self._controller.get_folder_items(project_name) @@ -461,6 +555,11 @@ class ProductsModel: if product_type_items is None: product_type_items = self.get_product_type_items(project_name) + if product_base_type_items is None: + product_base_type_items = self.get_product_base_type_items( + project_name + ) + loaded_product_ids = self._controller.get_loaded_product_ids() versions_by_product_id = collections.defaultdict(list) @@ -470,7 +569,13 @@ class ProductsModel: product_type_item.name: product_type_item for product_type_item in product_type_items } - output = {} + + product_base_type_items_by_name: dict[str, ProductBaseTypeItem] = { + product_base_type_item.name: product_base_type_item + for product_base_type_item in product_base_type_items + } + + output: dict[str, ProductItem] = {} for product in products: product_id = product["id"] folder_id = product["folderId"] @@ -484,6 +589,7 @@ class ProductsModel: product, versions, product_type_items_by_name, + product_base_type_items_by_name, folder_item.label, product_id in loaded_product_ids, )