mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge remote-tracking branch 'origin/feature/AY-7545_product-base-types' into feature/AY-7545_product-base-types
This commit is contained in:
commit
baba002af2
3 changed files with 150 additions and 108 deletions
|
|
@ -55,6 +55,7 @@ from .exceptions import (
|
|||
HostMissRequiredMethod,
|
||||
UnavailableSharedData,
|
||||
)
|
||||
from .product_name import ProductContext
|
||||
from .structures import ConvertorItem, InstanceContextInfo, PublishAttributes
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
|
@ -106,7 +107,7 @@ class CreatorOperationInfo:
|
|||
creator_identifier: str
|
||||
creator_label: str
|
||||
message: str
|
||||
traceback: str
|
||||
traceback: Optional[str]
|
||||
|
||||
|
||||
_NOT_SET = object()
|
||||
|
|
@ -247,7 +248,7 @@ class CreateContext: # noqa: PLR0904
|
|||
|
||||
# Publish context plugins attributes and it's values
|
||||
self._publish_attributes = PublishAttributes(self, {})
|
||||
self._original_context_data = {}
|
||||
self._original_context_data: dict[str, Any] = {}
|
||||
|
||||
# Validate host implementation
|
||||
# - defines if context is capable of handling context data
|
||||
|
|
@ -280,7 +281,7 @@ class CreateContext: # noqa: PLR0904
|
|||
self.headless = headless
|
||||
|
||||
# Instances by their ID
|
||||
self._instances_by_id = {}
|
||||
self._instances_by_id: dict[str, Any] = {}
|
||||
|
||||
self.creator_discover_result = None
|
||||
self.convertor_discover_result = None
|
||||
|
|
@ -419,7 +420,7 @@ class CreateContext: # noqa: PLR0904
|
|||
return set(IPublishHost.get_missing_publish_methods(host))
|
||||
|
||||
@property
|
||||
def host_is_valid(self):
|
||||
def host_is_valid(self) -> bool:
|
||||
"""Is host valid for creation."""
|
||||
return self._host_is_valid
|
||||
|
||||
|
|
@ -1204,6 +1205,12 @@ class CreateContext: # noqa: PLR0904
|
|||
task_entity = ayon_api.get_task_by_name(
|
||||
project_name, folder_entity["id"], current_task_name
|
||||
)
|
||||
# We require task in get_product_name so it is prudent to fail here
|
||||
# if task is not set. However, AYON itself doesn't require task so
|
||||
# maybe we should make it optional?
|
||||
if task_entity is None:
|
||||
msg = "Task was not found."
|
||||
raise CreatorError(msg)
|
||||
|
||||
if pre_create_data is None:
|
||||
pre_create_data = {}
|
||||
|
|
@ -1214,27 +1221,25 @@ class CreateContext: # noqa: PLR0904
|
|||
precreate_attr_defs = creator.get_pre_create_attr_defs()
|
||||
|
||||
# Create default values of precreate data
|
||||
_pre_create_data = get_default_values(precreate_attr_defs)
|
||||
pre_create_data = get_default_values(precreate_attr_defs)
|
||||
# Update passed precreate data to default values
|
||||
# TODO validate types
|
||||
_pre_create_data.update(pre_create_data)
|
||||
pre_create_data.update(pre_create_data)
|
||||
|
||||
project_entity = self.get_current_project_entity()
|
||||
args = (
|
||||
project_name,
|
||||
folder_entity,
|
||||
task_entity,
|
||||
variant,
|
||||
self.host_name,
|
||||
context = ProductContext(
|
||||
project_name=project_name,
|
||||
task_name=task_entity["name"] if task_entity else None,
|
||||
task_type=task_entity["taskType"] if task_entity else None,
|
||||
variant=variant,
|
||||
host_name=self.host_name,
|
||||
product_base_type=creator.product_base_type,
|
||||
)
|
||||
kwargs = {"project_entity": project_entity}
|
||||
# Backwards compatibility for 'project_entity' argument
|
||||
# - 'get_product_name' signature changed 24/07/08
|
||||
if not is_func_signature_supported(
|
||||
creator.get_product_name, *args, **kwargs
|
||||
):
|
||||
kwargs.pop("project_entity")
|
||||
product_name = creator.get_product_name(*args, **kwargs)
|
||||
|
||||
product_name = creator.get_product_name(
|
||||
context
|
||||
)
|
||||
|
||||
instance_data = {
|
||||
"folderPath": folder_entity["path"],
|
||||
|
|
@ -2403,7 +2408,7 @@ class CreateContext: # noqa: PLR0904
|
|||
|
||||
def _bulk_publish_attrs_change_finished(
|
||||
self,
|
||||
attr_info: Tuple[str, Union[str, None]],
|
||||
attr_info: Tuple[str, Optional[str]],
|
||||
sender: Optional[str],
|
||||
):
|
||||
if not attr_info:
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
"""Definition of creator plugins."""
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||
from typing import TYPE_CHECKING, Any, ClassVar, Optional
|
||||
|
||||
from ayon_core.lib import Logger, get_version_from_path
|
||||
from ayon_core.pipeline.plugin_discover import (
|
||||
|
|
@ -27,6 +28,9 @@ from .utils import get_next_versions_for_instances
|
|||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import logging
|
||||
|
||||
from ayon_core.host import HostBase
|
||||
from ayon_core.lib import AbstractAttrDef
|
||||
from .context import CreateContext, UpdateData # noqa: F401
|
||||
|
||||
|
|
@ -76,12 +80,18 @@ class ProductConvertorPlugin(ABC):
|
|||
return self._log
|
||||
|
||||
@property
|
||||
def host(self):
|
||||
def host(self) -> HostBase:
|
||||
"""Host definition.
|
||||
|
||||
Returns:
|
||||
HostBase: Host which initialized the plugin.
|
||||
|
||||
"""
|
||||
return self._create_context.host
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def identifier(self):
|
||||
def identifier(self) -> str:
|
||||
"""Converted identifier.
|
||||
|
||||
Returns:
|
||||
|
|
@ -89,7 +99,7 @@ class ProductConvertorPlugin(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def find_instances(self):
|
||||
def find_instances(self) -> None:
|
||||
"""Look for legacy instances in the scene.
|
||||
|
||||
Should call 'add_convertor_item' if there is at least one instance to
|
||||
|
|
@ -97,20 +107,21 @@ class ProductConvertorPlugin(ABC):
|
|||
"""
|
||||
|
||||
@abstractmethod
|
||||
def convert(self):
|
||||
def convert(self) -> None:
|
||||
"""Conversion code."""
|
||||
|
||||
@property
|
||||
def create_context(self):
|
||||
def create_context(self) -> CreateContext:
|
||||
"""Quick access to create context.
|
||||
|
||||
Returns:
|
||||
CreateContext: Context which initialized the plugin.
|
||||
|
||||
"""
|
||||
return self._create_context
|
||||
|
||||
@property
|
||||
def collection_shared_data(self):
|
||||
def collection_shared_data(self) -> dict[str, Any]:
|
||||
"""Access to shared data that can be used during 'find_instances'.
|
||||
|
||||
Returns:
|
||||
|
|
@ -118,23 +129,25 @@ class ProductConvertorPlugin(ABC):
|
|||
|
||||
Raises:
|
||||
UnavailableSharedData: When called out of collection phase.
|
||||
|
||||
"""
|
||||
return self._create_context.collection_shared_data
|
||||
|
||||
def add_convertor_item(self, label):
|
||||
def add_convertor_item(self, label: str) -> None:
|
||||
"""Add item to CreateContext.
|
||||
|
||||
Args:
|
||||
label (str): Label of item which will show in UI.
|
||||
|
||||
"""
|
||||
self._create_context.add_convertor_item(self.identifier, label)
|
||||
|
||||
def remove_convertor_item(self):
|
||||
def remove_convertor_item(self) -> None:
|
||||
"""Remove legacy item from create context when conversion finished."""
|
||||
self._create_context.remove_convertor_item(self.identifier)
|
||||
|
||||
|
||||
class BaseCreator(ABC):
|
||||
class BaseCreator(ABC): # noqa: PLR0904
|
||||
"""Plugin that create and modify instance data before publishing process.
|
||||
|
||||
We should maybe find better name as creation is only one part of its logic
|
||||
|
|
@ -173,7 +186,7 @@ class BaseCreator(ABC):
|
|||
# Instance attribute definitions that can be changed per instance
|
||||
# - returns list of attribute definitions from
|
||||
# `ayon_core.lib.attribute_definitions`
|
||||
instance_attr_defs: "list[AbstractAttrDef]" = []
|
||||
instance_attr_defs: ClassVar[list[AbstractAttrDef]] = []
|
||||
|
||||
# Filtering by host name - can be used to be filtered by host name
|
||||
# - used on all hosts when set to 'None' for Backwards compatibility
|
||||
|
|
@ -188,8 +201,20 @@ class BaseCreator(ABC):
|
|||
settings_name: Optional[str] = None
|
||||
|
||||
def __init__(
|
||||
self, project_settings, create_context, headless=False
|
||||
self,
|
||||
project_settings: dict[str, Any],
|
||||
create_context: CreateContext,
|
||||
*,
|
||||
headless: bool = False
|
||||
):
|
||||
"""Base creator constructor.
|
||||
|
||||
Args:
|
||||
project_settings (dict[str, Any]): Project settings.
|
||||
create_context (CreateContext): Context which initialized creator.
|
||||
headless (bool): Running in headless mode.
|
||||
|
||||
"""
|
||||
# Reference to CreateContext
|
||||
self.create_context = create_context
|
||||
self.project_settings = project_settings
|
||||
|
|
@ -202,7 +227,10 @@ class BaseCreator(ABC):
|
|||
self.register_callbacks()
|
||||
|
||||
@staticmethod
|
||||
def _get_settings_values(project_settings, category_name, plugin_name):
|
||||
def _get_settings_values(
|
||||
project_settings: dict[str, Any],
|
||||
category_name: str,
|
||||
plugin_name: str) -> Optional[dict[str, Any]]:
|
||||
"""Helper method to get settings values.
|
||||
|
||||
Args:
|
||||
|
|
@ -223,13 +251,18 @@ class BaseCreator(ABC):
|
|||
|
||||
return create_settings.get(plugin_name)
|
||||
|
||||
def apply_settings(self, project_settings):
|
||||
def apply_settings(self, project_settings: dict[str, Any]) -> None:
|
||||
"""Method called on initialization of plugin to apply settings.
|
||||
|
||||
Default implementation tries to auto-apply settings values if are
|
||||
in expected hierarchy.
|
||||
in expected hierarchy.
|
||||
|
||||
Args:
|
||||
project_settings (dict[str, Any]): Project settings.
|
||||
|
||||
Example:
|
||||
Data hierarchy to auto-apply settings::
|
||||
|
||||
Data hierarchy to auto-apply settings:
|
||||
├─ {self.settings_category} - Root key in settings
|
||||
│ └─ "create" - Hardcoded key
|
||||
│ └─ {self.settings_name} | {class name} - Name of plugin
|
||||
|
|
@ -238,7 +271,8 @@ class BaseCreator(ABC):
|
|||
It is mandatory to define 'settings_category' attribute. Attribute
|
||||
'settings_name' is optional and class name is used if is not defined.
|
||||
|
||||
Example data:
|
||||
Example data::
|
||||
|
||||
ProjectSettings {
|
||||
"maya": { # self.settings_category
|
||||
"create": { # Hardcoded key
|
||||
|
|
@ -257,6 +291,7 @@ class BaseCreator(ABC):
|
|||
|
||||
Args:
|
||||
project_settings (dict[str, Any]): Project settings.
|
||||
|
||||
"""
|
||||
settings_category = self.settings_category
|
||||
if not settings_category:
|
||||
|
|
@ -266,7 +301,9 @@ class BaseCreator(ABC):
|
|||
settings_name = self.settings_name or cls_name
|
||||
|
||||
settings = self._get_settings_values(
|
||||
project_settings, settings_category, settings_name
|
||||
project_settings: dict[str, Any],
|
||||
settings_category: str,
|
||||
settings_name: str
|
||||
)
|
||||
if settings is None:
|
||||
self.log.debug(f"No settings found for {cls_name}")
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"""Get product name template and calculate product name."""
|
||||
"""Functions for product name resolution."""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import copy
|
||||
from dataclasses import dataclass
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
import ayon_api
|
||||
|
|
@ -16,31 +18,52 @@ from .constants import DEFAULT_PRODUCT_TEMPLATE
|
|||
from .exceptions import TaskNotSetError, TemplateFillError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ayon_core.pipeline.product_base_types import ProductBaseType
|
||||
from ayon_core.pipeline.create.base_product_types import BaseProductType
|
||||
|
||||
|
||||
@dataclass
|
||||
class ProductContext:
|
||||
"""Product context for product name resolution.
|
||||
|
||||
To get the product name, we need to know the context in which the product
|
||||
is created. This context is defined by the project name, task name,
|
||||
task type, host name, product base type and variant. The context is
|
||||
passed to the `get_product_name` function, which uses it to resolve the
|
||||
product name based on the AYON settings.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
task_name (str): Task name.
|
||||
task_type (str): Task type.
|
||||
host_name (str): Host name.
|
||||
product_base_type (BaseProductType): Product base type.
|
||||
variant (str): Variant value.
|
||||
product_type (Optional[str]): Product type.
|
||||
|
||||
"""
|
||||
|
||||
project_name: str
|
||||
task_name: str
|
||||
task_type: str
|
||||
host_name: str
|
||||
product_base_type: BaseProductType
|
||||
variant: str
|
||||
product_type: Optional[str] = None
|
||||
|
||||
|
||||
def get_product_name_template(
|
||||
project_name: str,
|
||||
product_type: str,
|
||||
task_name: str,
|
||||
task_type: str,
|
||||
host_name: str,
|
||||
context: ProductContext,
|
||||
default_template: Optional[str] = None,
|
||||
project_settings: Optional[dict] = None
|
||||
) -> str:
|
||||
"""Get product name template based on passed context.
|
||||
|
||||
Args:
|
||||
project_name (str): Project on which the context lives.
|
||||
product_type (str): Product type for which the product name is
|
||||
calculated.
|
||||
host_name (str): Name of host in which the product name is calculated.
|
||||
task_name (str): Name of task in which context the product is created.
|
||||
task_type (str): Type of task in which context the product is created.
|
||||
default_template (Union[str, None]): Default template which is used if
|
||||
context (ProductContext): Product context.
|
||||
default_template (Optional[str]): Default template which is used if
|
||||
settings won't find any matching possibility. Constant
|
||||
'DEFAULT_PRODUCT_TEMPLATE' is used if not defined.
|
||||
project_settings (Union[Dict[str, Any], None]): Prepared settings for
|
||||
project_settings (Optional[Dict[str, Any]]): Prepared settings for
|
||||
project. Settings are queried if not passed.
|
||||
|
||||
Returns:
|
||||
|
|
@ -48,33 +71,24 @@ def get_product_name_template(
|
|||
|
||||
"""
|
||||
if project_settings is None:
|
||||
project_settings = get_project_settings(project_name)
|
||||
project_settings = get_project_settings(context.project_name)
|
||||
|
||||
if not context.product_type:
|
||||
context.product_type = context.product_base_type.name
|
||||
|
||||
tools_settings = project_settings["core"]["tools"]
|
||||
profiles = tools_settings["creator"]["product_name_profiles"]
|
||||
filtering_criteria = {
|
||||
"product_types": product_type,
|
||||
"hosts": host_name,
|
||||
"tasks": task_name,
|
||||
"task_types": task_type
|
||||
"product_types": context.product_type,
|
||||
"hosts": context.host_name,
|
||||
"tasks": context.task_name,
|
||||
"task_types": context.task_type
|
||||
}
|
||||
|
||||
matching_profile = filter_profiles(profiles, filtering_criteria)
|
||||
template = None
|
||||
if matching_profile:
|
||||
# TODO remove formatting keys replacement
|
||||
template = (
|
||||
matching_profile["template"]
|
||||
.replace("{task}", "{task[name]}")
|
||||
.replace("{Task}", "{Task[name]}")
|
||||
.replace("{TASK}", "{TASK[NAME]}")
|
||||
.replace("{family}", "{product[type]}")
|
||||
.replace("{Family}", "{Product[type]}")
|
||||
.replace("{FAMILY}", "{PRODUCT[TYPE]}")
|
||||
.replace("{asset}", "{folder[name]}")
|
||||
.replace("{Asset}", "{Folder[name]}")
|
||||
.replace("{ASSET}", "{FOLDER[NAME]}")
|
||||
)
|
||||
|
||||
template = matching_profile["template"]
|
||||
# Make sure template is set (matching may have empty string)
|
||||
if not template:
|
||||
template = default_template or DEFAULT_PRODUCT_TEMPLATE
|
||||
|
|
@ -82,18 +96,12 @@ def get_product_name_template(
|
|||
|
||||
|
||||
def get_product_name(
|
||||
project_name: str,
|
||||
task_name: str,
|
||||
task_type: str,
|
||||
host_name: str,
|
||||
product_type: str,
|
||||
variant: str,
|
||||
context: ProductContext,
|
||||
default_template: Optional[str] = None,
|
||||
dynamic_data: Optional[dict] = None,
|
||||
project_settings: Optional[dict] = None,
|
||||
product_type_filter: Optional[str] = None,
|
||||
project_entity: Optional[dict] = None,
|
||||
product_base_type: Optional[ProductBaseType] = None,
|
||||
) -> str:
|
||||
"""Calculate product name based on passed context and AYON settings.
|
||||
|
||||
|
|
@ -105,18 +113,8 @@ def get_product_name(
|
|||
That's main reason why so many arguments are required to calculate product
|
||||
name.
|
||||
|
||||
Todos:
|
||||
Find better filtering options to avoid requirement of
|
||||
argument 'family_filter'.
|
||||
|
||||
Args:
|
||||
project_name (str): Project name.
|
||||
task_name (Union[str, None]): Task name.
|
||||
task_type (Union[str, None]): Task type.
|
||||
host_name (str): Host name.
|
||||
product_type (str): Product type.
|
||||
product_base_type (ProductBaseType): Product base type.
|
||||
variant (str): In most of the cases it is user input during creation.
|
||||
context (ProductContext): Product context.
|
||||
default_template (Optional[str]): Default template if any profile does
|
||||
not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE'
|
||||
is used if is not passed.
|
||||
|
|
@ -139,56 +137,58 @@ def get_product_name(
|
|||
is not collected.
|
||||
|
||||
"""
|
||||
if not product_type:
|
||||
return ""
|
||||
# Product type was mandatory. If it is missing, use name from base type
|
||||
# to avoid breaking changes.
|
||||
if context.product_type is None:
|
||||
product_type = context.product_base_type.name
|
||||
|
||||
template_context = copy(context)
|
||||
if product_type_filter:
|
||||
template_context.product_type = product_type_filter
|
||||
|
||||
template = get_product_name_template(
|
||||
project_name,
|
||||
product_type_filter or product_type,
|
||||
task_name,
|
||||
task_type,
|
||||
host_name,
|
||||
template_context,
|
||||
default_template=default_template,
|
||||
project_settings=project_settings
|
||||
)
|
||||
# Simple check of task name existence for template with {task} in
|
||||
# - missing task should be possible only in Standalone publisher
|
||||
if not task_name and "{task" in template.lower():
|
||||
if not context.task_name and "{task" in template.lower():
|
||||
raise TaskNotSetError
|
||||
|
||||
task_value = {
|
||||
"name": task_name,
|
||||
"type": task_type,
|
||||
"name": context.task_name,
|
||||
"type": context.task_type,
|
||||
}
|
||||
|
||||
# task_value can be for backwards compatibility
|
||||
# single string or dict
|
||||
if "{task}" in template.lower():
|
||||
task_value = task_name # type: ignore[assignment]
|
||||
task_value = context.task_name # type: ignore[assignment]
|
||||
|
||||
elif "{task[short]}" in template.lower():
|
||||
if project_entity is None:
|
||||
project_entity = ayon_api.get_project(project_name)
|
||||
project_entity = ayon_api.get_project(context.project_name)
|
||||
task_types_by_name = {
|
||||
task["name"]: task for task in
|
||||
project_entity["taskTypes"]
|
||||
}
|
||||
task_short = task_types_by_name.get(task_type, {}).get("shortName")
|
||||
task_short = task_types_by_name.get(
|
||||
context.task_type, {}).get("shortName")
|
||||
task_value["short"] = task_short
|
||||
|
||||
fill_pairs = {
|
||||
"variant": variant,
|
||||
"variant": context.variant,
|
||||
"family": product_type,
|
||||
"task": task_value,
|
||||
"product": {
|
||||
"type": product_type,
|
||||
"base": product_base_type
|
||||
"base": context.product_base_type.name
|
||||
}
|
||||
}
|
||||
if dynamic_data:
|
||||
# Dynamic data may override default values
|
||||
for key, value in dynamic_data.items():
|
||||
fill_pairs[key] = value
|
||||
fill_pairs = dict(dynamic_data.items())
|
||||
|
||||
try:
|
||||
return StringTemplate.format_strict_template(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue