🔧 WIP on product base type support in loader tool

This commit is contained in:
Ondrej Samohel 2025-06-06 10:01:32 +02:00
parent fac933c16a
commit 6ea717bc36
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
2 changed files with 226 additions and 46 deletions

View file

@ -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)

View file

@ -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,
)