From bcdeba18ac65d401b9b90add03f4c8bdce3be1ee Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 2 Jun 2025 09:29:04 +0200 Subject: [PATCH 01/90] :wrench: implementation WIP --- client/ayon_core/pipeline/create/context.py | 18 +++++++++++ .../pipeline/create/creator_plugins.py | 32 ++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f0d9fa8927..e6cc4393c5 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -18,6 +18,7 @@ from typing import ( Callable, Union, ) +from warnings import warn import pyblish.logic import pyblish.api @@ -31,6 +32,7 @@ from ayon_core.host import IPublishHost, IWorkfileHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult +from ayon_core.pipeline import is_supporting_product_base_type from .exceptions import ( CreatorError, @@ -1194,6 +1196,22 @@ class CreateContext: "productType": creator.product_type, "variant": variant } + + # Add product base type if supported. + # TODO (antirotor): Once all creators support product base type + # remove this check. + if is_supporting_product_base_type(): + + if hasattr(creator, "product_base_type"): + instance_data["productBaseType"] = creator.product_base_type + else: + warn( + f"Creator {creator_identifier} does not support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2, + ) + if active is not None: if not isinstance(active, bool): self.log.warning( diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index cbc06145fb..ad4b777db5 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -3,6 +3,7 @@ import os import copy import collections from typing import TYPE_CHECKING, Optional, Dict, Any +from warnings import warn from abc import ABC, abstractmethod @@ -16,6 +17,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin_path ) from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir +from ayon_core.pipeline import is_supporting_product_base_type from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -308,6 +310,9 @@ class BaseCreator(ABC): Default implementation returns plugin's product type. """ + if is_supporting_product_base_type(): + return self.product_base_type + return self.product_type @property @@ -317,6 +322,16 @@ class BaseCreator(ABC): pass + @property + @abstractmethod + def product_base_type(self): + """Base product type that plugin represents. + + This is used to group products in UI. + """ + + pass + @property def project_name(self): """Current project name. @@ -378,7 +393,8 @@ class BaseCreator(ABC): self, product_name: str, data: Dict[str, Any], - product_type: Optional[str] = None + product_type: Optional[str] = None, + product_base_type: Optional[str] = None ) -> CreatedInstance: """Create instance and add instance to context. @@ -387,6 +403,8 @@ class BaseCreator(ABC): data (Dict[str, Any]): Instance data. product_type (Optional[str]): Product type, object attribute 'product_type' is used if not passed. + product_base_type (Optional[str]): Product base type, object + attribute 'product_type' is used if not passed. Returns: CreatedInstance: Created instance. @@ -394,6 +412,18 @@ class BaseCreator(ABC): """ if product_type is None: product_type = self.product_type + + if is_supporting_product_base_type() and not product_base_type: + if not self.product_base_type: + warn( + f"Creator {self.identifier} does not support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2, + ) + else: + product_base_type = self.product_base_type + instance = CreatedInstance( product_type, product_name, From 9e730a6b5b1ecdf1cc67ea8609034e43929ab303 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 3 Jun 2025 10:58:43 +0200 Subject: [PATCH 02/90] :wrench: changes in Plugin anc CreateInstance WIP --- .../pipeline/create/creator_plugins.py | 63 +++++++++++-------- .../ayon_core/pipeline/create/structures.py | 20 ++++++ 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index ad4b777db5..bb824f52e3 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -17,7 +17,7 @@ from ayon_core.pipeline.plugin_discover import ( deregister_plugin_path ) from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir -from ayon_core.pipeline import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name @@ -308,12 +308,15 @@ class BaseCreator(ABC): """Identifier of creator (must be unique). Default implementation returns plugin's product type. + """ - + identifier = self.product_type if is_supporting_product_base_type(): - return self.product_base_type + identifier = self.product_base_type + if self.product_type: + identifier = f"{identifier}.{self.product_type}" + return identifier - return self.product_type @property @abstractmethod @@ -323,14 +326,19 @@ class BaseCreator(ABC): pass @property - @abstractmethod - def product_base_type(self): + def product_base_type(self) -> Optional[str]: """Base product type that plugin represents. - This is used to group products in UI. - """ + Todo (antirotor): This should be required in future - it + should be made abstract then. - pass + Returns: + Optional[str]: Base product type that plugin represents. + If not set, it is assumed that the creator plugin is obsolete + and does not support product base type. + + """ + return None @property def project_name(self): @@ -361,13 +369,14 @@ class BaseCreator(ABC): Default implementation use attributes in this order: - 'group_label' -> 'label' -> 'identifier' - Keep in mind that 'identifier' use 'product_type' by default. + + Keep in mind that 'identifier' uses 'product_base_type' by default. Returns: str: Group label that can be used for grouping of instances in UI. - Group label can be overridden by instance itself. + Group label can be overridden by the instance itself. + """ - if self._cached_group_label is None: label = self.identifier if self.group_label: @@ -413,22 +422,26 @@ class BaseCreator(ABC): if product_type is None: product_type = self.product_type - if is_supporting_product_base_type() and not product_base_type: - if not self.product_base_type: - warn( - f"Creator {self.identifier} does not support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2, - ) - else: - product_base_type = self.product_base_type + if ( + is_supporting_product_base_type() + and not product_base_type + and not self.product_base_type + ): + warn( + f"Creator {self.identifier} does not support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2, + ) + else: + product_base_type = self.product_base_type instance = CreatedInstance( - product_type, - product_name, - data, + product_type=product_type, + product_name=product_name, + data=data, creator=self, + product_base_type=product_base_type ) self._add_instance_to_context(instance) return instance diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index d7ba6b9c24..389ce25961 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -3,6 +3,7 @@ import collections from uuid import uuid4 import typing from typing import Optional, Dict, List, Any +import warnings from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -10,6 +11,9 @@ from ayon_core.lib.attribute_definitions import ( serialize_attr_defs, deserialize_attr_defs, ) + +from ayon_core.pipeline.compatibility import is_supporting_product_base_type + from ayon_core.pipeline import ( AYON_INSTANCE_ID, AVALON_INSTANCE_ID, @@ -471,6 +475,7 @@ class CreatedInstance: "id", "instance_id", "productType", + "productBaseType", "creator_identifier", "creator_attributes", "publish_attributes" @@ -490,7 +495,17 @@ class CreatedInstance: data: Dict[str, Any], creator: "BaseCreator", transient_data: Optional[Dict[str, Any]] = None, + product_base_type: Optional[str] = None ): + + if is_supporting_product_base_type() and product_base_type is None: + warnings.warn( + f"Creator {creator!r} doesn't support " + "product base type. This will be required in future.", + DeprecationWarning, + stacklevel=2 + ) + self._creator = creator creator_identifier = creator.identifier group_label = creator.get_group_label() @@ -540,6 +555,11 @@ class CreatedInstance: self._data["id"] = item_id self._data["productType"] = product_type self._data["productName"] = product_name + + if is_supporting_product_base_type(): + data.pop("productBaseType", None) + self._data["productBaseType"] = product_base_type + self._data["active"] = data.get("active", True) self._data["creator_identifier"] = creator_identifier From d237e5f54cd466957699a350e9f8978a743eac7f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 3 Jun 2025 17:25:32 +0200 Subject: [PATCH 03/90] :art: add support for product base type to basic creator logic --- client/ayon_core/pipeline/create/context.py | 2 +- .../pipeline/create/creator_plugins.py | 23 ++-- .../ayon_core/pipeline/create/product_name.py | 117 ++++++++++++------ .../ayon_core/pipeline/create/structures.py | 26 ++-- .../tools/publisher/models/create.py | 7 ++ 5 files changed, 119 insertions(+), 56 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index e6cc4393c5..f267450543 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -32,7 +32,7 @@ from ayon_core.host import IPublishHost, IWorkfileHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult -from ayon_core.pipeline import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from .exceptions import ( CreatorError, diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index bb824f52e3..155a443b53 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -317,7 +317,6 @@ class BaseCreator(ABC): identifier = f"{identifier}.{self.product_type}" return identifier - @property @abstractmethod def product_type(self): @@ -562,14 +561,15 @@ class BaseCreator(ABC): def get_product_name( self, - project_name, - folder_entity, - task_entity, - variant, - host_name=None, - instance=None, - project_entity=None, - ): + project_name: str, + folder_entity: dict[str, Any], + task_entity: dict[str, Any], + variant: str, + host_name: Optional[str] = None, + instance: Optional[CreatedInstance] = None, + project_entity: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None, + ) -> str: """Return product name for passed context. Method is also called on product name update. In that case origin @@ -586,8 +586,12 @@ class BaseCreator(ABC): for which is product name updated. Passed only on product name update. project_entity (Optional[dict[str, Any]]): Project entity. + product_base_type (Optional[str]): Product base type. """ + if is_supporting_product_base_type() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 + product_base_type = instance.product_base_type + if host_name is None: host_name = self.create_context.host_name @@ -619,6 +623,7 @@ class BaseCreator(ABC): dynamic_data=dynamic_data, project_settings=self.project_settings, project_entity=project_entity, + product_base_type=product_base_type ) def get_instance_attr_defs(self): diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ecffa4a340..3fdf786b0e 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,9 +1,16 @@ +"""Functions for handling product names.""" +from __future__ import annotations + +from typing import Any, Optional, Union +from warnings import warn + import ayon_api from ayon_core.lib import ( StringTemplate, filter_profiles, prepare_template_data, ) +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -11,14 +18,15 @@ from .exceptions import TaskNotSetError, TemplateFillError def get_product_name_template( - project_name, - product_type, - task_name, - task_type, - host_name, - default_template=None, - project_settings=None -): + project_name: str, + product_type: str, + task_name: str, + task_type: str, + host_name: str, + default_template: Optional[str] = None, + project_settings: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None +) -> str: """Get product name template based on passed context. Args: @@ -28,13 +36,17 @@ def get_product_name_template( 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 + 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 are queried if not passed. - """ + product_base_type (Optional[str]): Base type of product. + Returns: + str: Product name template. + + """ if project_settings is None: project_settings = get_project_settings(project_name) tools_settings = project_settings["core"]["tools"] @@ -46,6 +58,15 @@ def get_product_name_template( "task_types": task_type } + if is_supporting_product_base_type(): + if product_base_type: + filtering_criteria["product_base_types"] = product_base_type + else: + warn( + "Product base type is not provided, please update your" + "creation code to include it. It will be required in " + "the future.", DeprecationWarning, stacklevel=2) + matching_profile = filter_profiles(profiles, filtering_criteria) template = None if matching_profile: @@ -70,17 +91,18 @@ def get_product_name_template( def get_product_name( - project_name, - task_name, - task_type, - host_name, - product_type, - variant, - default_template=None, - dynamic_data=None, - project_settings=None, - product_type_filter=None, - project_entity=None, + project_name: str, + task_name: str, + task_type: str, + host_name: str, + product_type: str, + variant: str, + default_template: Optional[str] = None, + dynamic_data: Optional[dict[str, Any]] = None, + project_settings: Optional[dict[str, Any]] = None, + product_type_filter: Optional[str] = None, + project_entity: Optional[dict[str, Any]] = None, + product_base_type: Optional[str] = None ): """Calculate product name based on passed context and AYON settings. @@ -92,14 +114,20 @@ def get_product_name( That's main reason why so many arguments are required to calculate product name. + Deprecation: + The `product_base_type` argument is optional now, but it will be + mandatory in future versions. It is recommended to pass it now to + avoid issues in the future. If it is not passed, a warning will be raised + to inform about this change. + 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. + task_name (str): Task name. + task_type (str): Task type. host_name (str): Host name. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. @@ -115,6 +143,8 @@ def get_product_name( not passed. project_entity (Optional[Dict[str, Any]]): Project entity used when task short name is required by template. + product_base_type (Optional[str]): Base type of product. + This will be mandatory in future versions. Returns: str: Product name. @@ -129,13 +159,14 @@ def get_product_name( return "" template = get_product_name_template( - project_name, - product_type_filter or product_type, - task_name, - task_type, - host_name, + project_name=project_name, + product_type=product_type_filter or product_type, + task_name=task_name, + task_type=task_type, + host_name=host_name, default_template=default_template, - project_settings=project_settings + project_settings=project_settings, + product_base_type=product_base_type, ) # Simple check of task name existence for template with {task} in # - missing task should be possible only in Standalone publisher @@ -147,7 +178,7 @@ def get_product_name( "type": task_type, } if "{task}" in template.lower(): - task_value = task_name + task_value["name"] = task_name elif "{task[short]}" in template.lower(): if project_entity is None: @@ -159,14 +190,25 @@ def get_product_name( task_short = task_types_by_name.get(task_type, {}).get("shortName") task_value["short"] = task_short - fill_pairs = { + # look what we have to do to make mypy happy. We should stop using + # those undefined dict based types. + product: dict[str, str] = {"type": product_type} + if is_supporting_product_base_type(): + if product_base_type: + product["baseType"] = product_base_type + elif "{product[basetype]}" in template.lower(): + warn( + "You have Product base type in product name template," + "but it is not provided by the creator, please update your" + "creation code to include it. It will be required in " + "the future.", DeprecationWarning, stacklevel=2) + fill_pairs: dict[str, Union[str, dict[str, str]]] = { "variant": variant, "family": product_type, "task": task_value, - "product": { - "type": product_type - } + "product": product, } + if dynamic_data: # Dynamic data may override default values for key, value in dynamic_data.items(): @@ -178,7 +220,8 @@ def get_product_name( data=prepare_template_data(fill_pairs) ) except KeyError as exp: - raise TemplateFillError( - "Value for {} key is missing in template '{}'." - " Available values are {}".format(str(exp), template, fill_pairs) + msg = ( + f"Value for {exp} key is missing in template '{template}'." + f" Available values are {fill_pairs}" ) + raise TemplateFillError(msg) from exp diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 389ce25961..a6b57c29ca 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -3,7 +3,7 @@ import collections from uuid import uuid4 import typing from typing import Optional, Dict, List, Any -import warnings +from warnings import warn from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -465,6 +465,10 @@ class CreatedInstance: data (Dict[str, Any]): Data used for filling product name or override data from already existing instance. creator (BaseCreator): Creator responsible for instance. + product_base_type (Optional[str]): Product base type that will be + created. If not provided then product base type is taken from + creator plugin. If creator does not have product base type then + deprecation warning is raised. """ # Keys that can't be changed or removed from data after loading using @@ -497,14 +501,18 @@ class CreatedInstance: transient_data: Optional[Dict[str, Any]] = None, product_base_type: Optional[str] = None ): - - if is_supporting_product_base_type() and product_base_type is None: - warnings.warn( - f"Creator {creator!r} doesn't support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2 - ) + """Initialize CreatedInstance.""" + if is_supporting_product_base_type(): + if not hasattr(creator, "product_base_type"): + warn( + f"Provided creator {creator!r} doesn't have " + "product base type attribute defined. This will be " + "required in future.", + DeprecationWarning, + stacklevel=2 + ) + elif not product_base_type: + product_base_type = creator.product_base_type self._creator = creator creator_identifier = creator.identifier diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 900168eaef..862bd1ea03 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -34,6 +34,8 @@ from ayon_core.pipeline.create import ( ConvertorsOperationFailed, ConvertorItem, ) +from ayon_core.pipeline.compatibility import is_supporting_product_base_type + from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, CardMessageTypes, @@ -631,12 +633,17 @@ class CreateModel: "instance": instance, "project_entity": project_entity, } + + if is_supporting_product_base_type() and hasattr(creator, "product_base_type"): # noqa: E501 + kwargs["product_base_type"] = creator.product_base_type + # 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") + kwargs.pop("product_base_type") return creator.get_product_name(*args, **kwargs) def create( From 67db5c123ff72b41b53209310c6bae339de1d8ed Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 4 Jun 2025 11:24:22 +0200 Subject: [PATCH 04/90] :dog: linter fixes --- .../pipeline/create/creator_plugins.py | 99 +++++-------------- 1 file changed, 25 insertions(+), 74 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 155a443b53..040ed073f2 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,32 +1,32 @@ -# -*- coding: utf-8 -*- -import os -import copy +"""Creator plugins for the create process.""" import collections -from typing import TYPE_CHECKING, Optional, Dict, Any +import copy +import os +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Dict, Optional from warnings import warn -from abc import ABC, abstractmethod - -from ayon_core.settings import get_project_settings from ayon_core.lib import Logger, get_version_from_path +from ayon_core.pipeline.compatibility import is_supporting_product_base_type from ayon_core.pipeline.plugin_discover import ( + deregister_plugin, + deregister_plugin_path, discover, register_plugin, register_plugin_path, - deregister_plugin, - deregister_plugin_path ) -from ayon_core.pipeline.staging_dir import get_staging_dir_info, StagingDir -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.staging_dir import StagingDir, get_staging_dir_info +from ayon_core.settings import get_project_settings from .constants import DEFAULT_VARIANT_VALUE -from .product_name import get_product_name -from .utils import get_next_versions_for_instances from .legacy_create import LegacyCreator +from .product_name import get_product_name from .structures import CreatedInstance +from .utils import get_next_versions_for_instances if TYPE_CHECKING: from ayon_core.lib import AbstractAttrDef + # Avoid cyclic imports from .context import CreateContext, UpdateData # noqa: F401 @@ -70,7 +70,6 @@ class ProductConvertorPlugin(ABC): Returns: logging.Logger: Logger with name of the plugin. """ - if self._log is None: self._log = Logger.get_logger(self.__class__.__name__) return self._log @@ -86,9 +85,8 @@ class ProductConvertorPlugin(ABC): Returns: str: Converted identifier unique for all converters in host. - """ - pass + """ @abstractmethod def find_instances(self): @@ -98,14 +96,10 @@ class ProductConvertorPlugin(ABC): convert. """ - pass - @abstractmethod def convert(self): """Conversion code.""" - pass - @property def create_context(self): """Quick access to create context. @@ -113,7 +107,6 @@ class ProductConvertorPlugin(ABC): Returns: CreateContext: Context which initialized the plugin. """ - return self._create_context @property @@ -126,7 +119,6 @@ class ProductConvertorPlugin(ABC): Raises: UnavailableSharedData: When called out of collection phase. """ - return self._create_context.collection_shared_data def add_convertor_item(self, label): @@ -135,12 +127,10 @@ class ProductConvertorPlugin(ABC): 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): """Remove legacy item from create context when conversion finished.""" - self._create_context.remove_convertor_item(self.identifier) @@ -159,7 +149,6 @@ class BaseCreator(ABC): create_context (CreateContext): Context which initialized creator. headless (bool): Running in headless mode. """ - # Label shown in UI label = None group_label = None @@ -223,7 +212,6 @@ class BaseCreator(ABC): Returns: Optional[dict[str, Any]]: Settings values or None. """ - settings = project_settings.get(category_name) if not settings: return None @@ -269,7 +257,6 @@ class BaseCreator(ABC): Args: project_settings (dict[str, Any]): Project settings. """ - settings_category = self.settings_category if not settings_category: return @@ -281,18 +268,17 @@ class BaseCreator(ABC): project_settings, settings_category, settings_name ) if settings is None: - self.log.debug("No settings found for {}".format(cls_name)) + self.log.debug(f"No settings found for {cls_name}") return for key, value in settings.items(): # Log out attributes that are not defined on plugin object # - those may be potential dangerous typos in settings if not hasattr(self, key): - self.log.debug(( - "Applying settings to unknown attribute '{}' on '{}'." - ).format( + self.log.debug( + "Applying settings to unknown attribute '%' on '%'.", key, cls_name - )) + ) setattr(self, key, value) def register_callbacks(self): @@ -301,7 +287,6 @@ class BaseCreator(ABC): Default implementation does nothing. It can be overridden to register callbacks for creator. """ - pass @property def identifier(self): @@ -322,8 +307,6 @@ class BaseCreator(ABC): def product_type(self): """Family that plugin represents.""" - pass - @property def product_base_type(self) -> Optional[str]: """Base product type that plugin represents. @@ -346,7 +329,6 @@ class BaseCreator(ABC): Returns: str: Name of a project. """ - return self.create_context.project_name @property @@ -356,7 +338,6 @@ class BaseCreator(ABC): Returns: Anatomy: Project anatomy object. """ - return self.create_context.project_anatomy @property @@ -368,13 +349,13 @@ class BaseCreator(ABC): Default implementation use attributes in this order: - 'group_label' -> 'label' -> 'identifier' - + Keep in mind that 'identifier' uses 'product_base_type' by default. Returns: str: Group label that can be used for grouping of instances in UI. Group label can be overridden by the instance itself. - + """ if self._cached_group_label is None: label = self.identifier @@ -392,7 +373,6 @@ class BaseCreator(ABC): Returns: logging.Logger: Logger with name of the plugin. """ - if self._log is None: self._log = Logger.get_logger(self.__class__.__name__) return self._log @@ -456,7 +436,6 @@ class BaseCreator(ABC): Args: instance (CreatedInstance): New created instance. """ - self.create_context.creator_adds_instance(instance) def _remove_instance_from_context(self, instance): @@ -469,7 +448,6 @@ class BaseCreator(ABC): Args: instance (CreatedInstance): Instance which should be removed. """ - self.create_context.creator_removed_instance(instance) @abstractmethod @@ -481,8 +459,6 @@ class BaseCreator(ABC): implementation """ - pass - @abstractmethod def collect_instances(self): """Collect existing instances related to this creator plugin. @@ -508,8 +484,6 @@ class BaseCreator(ABC): ``` """ - pass - @abstractmethod def update_instances(self, update_list): """Store changes of existing instances so they can be recollected. @@ -519,8 +493,6 @@ class BaseCreator(ABC): contain changed instance and it's changes. """ - pass - @abstractmethod def remove_instances(self, instances): """Method called on instance removal. @@ -533,14 +505,11 @@ class BaseCreator(ABC): removed. """ - pass - def get_icon(self): """Icon of creator (product type). Can return path to image file or awesome icon name. """ - return self.icon def get_dynamic_data( @@ -556,7 +525,6 @@ class BaseCreator(ABC): These may be dynamically created based on current context of workfile. """ - return {} def get_product_name( @@ -633,15 +601,15 @@ class BaseCreator(ABC): and values are stored to metadata for future usage and for publishing purposes. - NOTE: - Convert method should be implemented which should care about updating - keys/values when plugin attributes change. + Note: + Convert method should be implemented which should care about + updating keys/values when plugin attributes change. Returns: list[AbstractAttrDef]: Attribute definitions that can be tweaked for created instance. - """ + """ return self.instance_attr_defs def get_attr_defs_for_instance(self, instance): @@ -664,12 +632,10 @@ class BaseCreator(ABC): Raises: UnavailableSharedData: When called out of collection phase. """ - return self.create_context.collection_shared_data def set_instance_thumbnail_path(self, instance_id, thumbnail_path=None): """Set path to thumbnail for instance.""" - self.create_context.thumbnail_paths_by_instance_id[instance_id] = ( thumbnail_path ) @@ -690,7 +656,6 @@ class BaseCreator(ABC): Returns: dict[str, int]: Next versions by instance id. """ - return get_next_versions_for_instances( self.create_context.project_name, instances ) @@ -757,7 +722,6 @@ class Creator(BaseCreator): int: Order in which is creator shown (less == earlier). By default is using Creator's 'order' or processing. """ - return self.order @abstractmethod @@ -772,11 +736,9 @@ class Creator(BaseCreator): pre_create_data(dict): Data based on pre creation attributes. Those may affect how creator works. """ - # instance = CreatedInstance( # self.product_type, product_name, instance_data # ) - pass def get_description(self): """Short description of product type and plugin. @@ -784,7 +746,6 @@ class Creator(BaseCreator): Returns: str: Short description of product type. """ - return self.description def get_detail_description(self): @@ -795,7 +756,6 @@ class Creator(BaseCreator): Returns: str: Detailed description of product type for artist. """ - return self.detailed_description def get_default_variants(self): @@ -809,7 +769,6 @@ class Creator(BaseCreator): Returns: list[str]: Whisper variants for user input. """ - return copy.deepcopy(self.default_variants) def get_default_variant(self, only_explicit=False): @@ -829,7 +788,6 @@ class Creator(BaseCreator): Returns: str: Variant value. """ - if only_explicit or self._default_variant: return self._default_variant @@ -850,7 +808,6 @@ class Creator(BaseCreator): Returns: str: Variant value. """ - return self.get_default_variant() def _set_default_variant_wrap(self, variant): @@ -862,7 +819,6 @@ class Creator(BaseCreator): Args: variant (str): New default variant value. """ - self._default_variant = variant default_variant = property( @@ -1012,7 +968,6 @@ class AutoCreator(BaseCreator): def remove_instances(self, instances): """Skip removal.""" - pass def discover_creator_plugins(*args, **kwargs): @@ -1036,9 +991,7 @@ def discover_legacy_creator_plugins(): plugin.apply_settings(project_settings) except Exception: log.warning( - "Failed to apply settings to creator {}".format( - plugin.__name__ - ), + "Failed to apply settings to creator %s", plugin.__name__, exc_info=True ) return plugins @@ -1055,7 +1008,6 @@ def get_legacy_creator_by_name(creator_name, case_sensitive=False): Returns: Creator: Return first matching plugin or `None`. """ - # Lower input creator name if is not case sensitive if not case_sensitive: creator_name = creator_name.lower() @@ -1127,7 +1079,6 @@ def cache_and_get_instances(creator, shared_key, list_instances_func): dict[str, dict[str, Any]]: Cached instances by creator identifier from result of passed function. """ - if shared_key not in creator.collection_shared_data: value = collections.defaultdict(list) for instance in list_instances_func(): From fce1ef248d4292b80a479eb156326aa002d4c48f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 4 Jun 2025 11:28:07 +0200 Subject: [PATCH 05/90] :dog: some more linter fixes --- client/ayon_core/pipeline/create/product_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 3fdf786b0e..1f0e8f3ba5 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -117,8 +117,8 @@ def get_product_name( Deprecation: The `product_base_type` argument is optional now, but it will be mandatory in future versions. It is recommended to pass it now to - avoid issues in the future. If it is not passed, a warning will be raised - to inform about this change. + avoid issues in the future. If it is not passed, a warning will be + raised to inform about this change. Todos: Find better filtering options to avoid requirement of From dfd8fe6e8cd9fbd5fe6e930189065623183b3020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 6 Jun 2025 10:33:25 +0200 Subject: [PATCH 06/90] :bug: report correctly skipped abstract creators --- client/ayon_core/pipeline/create/context.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f267450543..fcc18555b5 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -731,13 +731,12 @@ class CreateContext: manual_creators = {} report = discover_creator_plugins(return_report=True) self.creator_discover_result = report - for creator_class in report.plugins: - if inspect.isabstract(creator_class): - self.log.debug( - "Skipping abstract Creator {}".format(str(creator_class)) - ) - continue + for creator_class in report.abstract_plugins: + self.log.debug( + f"Skipping abstract Creator '%s'", str(creator_class) + ) + for creator_class in report.plugins: creator_identifier = creator_class.identifier if creator_identifier in creators: self.log.warning( From fa8c05488943dcd4f8195b4bcffc83e9cd45c37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 9 Jun 2025 13:54:41 +0200 Subject: [PATCH 07/90] :recycle: refactor support feature check function name --- client/ayon_core/pipeline/create/context.py | 4 ++-- client/ayon_core/pipeline/create/creator_plugins.py | 8 ++++---- client/ayon_core/pipeline/create/product_name.py | 6 +++--- client/ayon_core/pipeline/create/structures.py | 6 +++--- client/ayon_core/tools/publisher/models/create.py | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index fcc18555b5..a5a9d5a64a 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -32,7 +32,7 @@ from ayon_core.host import IPublishHost, IWorkfileHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from .exceptions import ( CreatorError, @@ -1199,7 +1199,7 @@ class CreateContext: # Add product base type if supported. # TODO (antirotor): Once all creators support product base type # remove this check. - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if hasattr(creator, "product_base_type"): instance_data["productBaseType"] = creator.product_base_type diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 040ed073f2..78fb723567 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -7,7 +7,7 @@ from typing import TYPE_CHECKING, Any, Dict, Optional from warnings import warn from ayon_core.lib import Logger, get_version_from_path -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path, @@ -296,7 +296,7 @@ class BaseCreator(ABC): """ identifier = self.product_type - if is_supporting_product_base_type(): + if is_product_base_type_supported(): identifier = self.product_base_type if self.product_type: identifier = f"{identifier}.{self.product_type}" @@ -402,7 +402,7 @@ class BaseCreator(ABC): product_type = self.product_type if ( - is_supporting_product_base_type() + is_product_base_type_supported() and not product_base_type and not self.product_base_type ): @@ -557,7 +557,7 @@ class BaseCreator(ABC): product_base_type (Optional[str]): Product base type. """ - if is_supporting_product_base_type() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 + if is_product_base_type_supported() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 product_base_type = instance.product_base_type if host_name is None: diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 1f0e8f3ba5..ab7de0c9e8 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -10,7 +10,7 @@ from ayon_core.lib import ( filter_profiles, prepare_template_data, ) -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -58,7 +58,7 @@ def get_product_name_template( "task_types": task_type } - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if product_base_type: filtering_criteria["product_base_types"] = product_base_type else: @@ -193,7 +193,7 @@ def get_product_name( # look what we have to do to make mypy happy. We should stop using # those undefined dict based types. product: dict[str, str] = {"type": product_type} - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if product_base_type: product["baseType"] = product_base_type elif "{product[basetype]}" in template.lower(): diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index a6b57c29ca..aad85a546a 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -12,7 +12,7 @@ from ayon_core.lib.attribute_definitions import ( deserialize_attr_defs, ) -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline import ( AYON_INSTANCE_ID, @@ -502,7 +502,7 @@ class CreatedInstance: product_base_type: Optional[str] = None ): """Initialize CreatedInstance.""" - if is_supporting_product_base_type(): + if is_product_base_type_supported(): if not hasattr(creator, "product_base_type"): warn( f"Provided creator {creator!r} doesn't have " @@ -564,7 +564,7 @@ class CreatedInstance: self._data["productType"] = product_type self._data["productName"] = product_name - if is_supporting_product_base_type(): + if is_product_base_type_supported(): data.pop("productBaseType", None) self._data["productBaseType"] = product_base_type diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 862bd1ea03..77e50dc788 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -34,7 +34,7 @@ from ayon_core.pipeline.create import ( ConvertorsOperationFailed, ConvertorItem, ) -from ayon_core.pipeline.compatibility import is_supporting_product_base_type +from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, @@ -634,7 +634,7 @@ class CreateModel: "project_entity": project_entity, } - if is_supporting_product_base_type() and hasattr(creator, "product_base_type"): # noqa: E501 + if is_product_base_type_supported() and hasattr(creator, "product_base_type"): # noqa: E501 kwargs["product_base_type"] = creator.product_base_type # Backwards compatibility for 'project_entity' argument From da286e3cfb5e8fb76a7328e5aab5de685316bb95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 11:23:37 +0200 Subject: [PATCH 08/90] :recycle: remove check for attribute --- client/ayon_core/pipeline/create/context.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a5a9d5a64a..a37fefc1f9 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -1201,15 +1201,14 @@ class CreateContext: # remove this check. if is_product_base_type_supported(): - if hasattr(creator, "product_base_type"): - instance_data["productBaseType"] = creator.product_base_type - else: - warn( - f"Creator {creator_identifier} does not support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2, + instance_data["productBaseType"] = creator.product_base_type + if creator.product_base_type is None: + msg = ( + f"Creator {creator_identifier} does not set " + "product base type. This will be required in future." ) + warn(msg, DeprecationWarning, stacklevel=2) + self.log.warning(msg) if active is not None: if not isinstance(active, bool): From e2a413f20eb64001703c2ff5220d232fbdf0d0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 11:40:43 +0200 Subject: [PATCH 09/90] :dog: remove unneeded f-string --- client/ayon_core/pipeline/create/context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a37fefc1f9..17a5dea7dc 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -733,7 +733,7 @@ class CreateContext: self.creator_discover_result = report for creator_class in report.abstract_plugins: self.log.debug( - f"Skipping abstract Creator '%s'", str(creator_class) + "Skipping abstract Creator '%s'", str(creator_class) ) for creator_class in report.plugins: From 50045d71bd58466e4e1b94209fda8cedafa69d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 10 Jun 2025 12:16:49 +0200 Subject: [PATCH 10/90] :sparkles: support product base types in the integrator --- client/ayon_core/pipeline/publish/lib.py | 6 +++++- client/ayon_core/plugins/publish/integrate.py | 9 +++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 49ecab2221..fba7f1d84b 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -106,7 +106,8 @@ def get_publish_template_name( task_type, project_settings=None, hero=False, - logger=None + logger=None, + product_base_type: Optional[str] = None ): """Get template name which should be used for passed context. @@ -126,6 +127,8 @@ def get_publish_template_name( hero (bool): Template is for hero version publishing. logger (logging.Logger): Custom logger used for 'filter_profiles' function. + product_base_type (Optional[str]): Product type for which should + be found template. Returns: str: Template name which should be used for integration. @@ -135,6 +138,7 @@ def get_publish_template_name( filter_criteria = { "hosts": host_name, "product_types": product_type, + "product_base_types": product_base_type, "task_names": task_name, "task_types": task_type, } diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index f1e066018c..41e71207e7 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -368,6 +368,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): folder_entity = instance.data["folderEntity"] product_name = instance.data["productName"] product_type = instance.data["productType"] + product_base_type = instance.data.get("productBaseType") + self.log.debug("Product: {}".format(product_name)) # Get existing product if it exists @@ -401,7 +403,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): folder_entity["id"], data=data, attribs=attributes, - entity_id=product_id + entity_id=product_id, + product_base_type=product_base_type ) if existing_product_entity is None: @@ -917,6 +920,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): host_name = context.data["hostName"] anatomy_data = instance.data["anatomyData"] product_type = instance.data["productType"] + product_base_type = instance.data.get("productBaseType") task_info = anatomy_data.get("task") or {} return get_publish_template_name( @@ -926,7 +930,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): task_name=task_info.get("name"), task_type=task_info.get("type"), project_settings=context.data["project_settings"], - logger=self.log + logger=self.log, + product_base_type=product_base_type ) def get_rootless_path(self, anatomy, path): From 2597469b30bfa1fc386218d0aa3a51154445ec52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 3 Sep 2025 15:16:27 +0200 Subject: [PATCH 11/90] :fire: remove deprecated code --- .../pipeline/create/creator_plugins.py | 46 ------------------- 1 file changed, 46 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 95db3f260f..26dbc5f3d3 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -977,52 +977,6 @@ def discover_convertor_plugins(*args, **kwargs): return discover(ProductConvertorPlugin, *args, **kwargs) -def discover_legacy_creator_plugins(): - from ayon_core.pipeline import get_current_project_name - - log = Logger.get_logger("CreatorDiscover") - - plugins = discover(LegacyCreator) - project_name = get_current_project_name() - project_settings = get_project_settings(project_name) - for plugin in plugins: - try: - plugin.apply_settings(project_settings) - except Exception: - log.warning( - "Failed to apply settings to creator %s", plugin.__name__, - exc_info=True - ) - return plugins - - -def get_legacy_creator_by_name(creator_name, case_sensitive=False): - """Find creator plugin by name. - - Args: - creator_name (str): Name of creator class that should be returned. - case_sensitive (bool): Match of creator plugin name is case sensitive. - Set to `False` by default. - - Returns: - Creator: Return first matching plugin or `None`. - """ - # Lower input creator name if is not case sensitive - if not case_sensitive: - creator_name = creator_name.lower() - - for creator_plugin in discover_legacy_creator_plugins(): - _creator_name = creator_plugin.__name__ - - # Lower creator plugin name if is not case sensitive - if not case_sensitive: - _creator_name = _creator_name.lower() - - if _creator_name == creator_name: - return creator_plugin - return None - - def register_creator_plugin(plugin): if issubclass(plugin, BaseCreator): register_plugin(BaseCreator, plugin) From 51965a9de160caf8fa334043b5f62adfaab1d374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Wed, 3 Sep 2025 15:18:50 +0200 Subject: [PATCH 12/90] :fire: remove unused import --- client/ayon_core/pipeline/create/creator_plugins.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 26dbc5f3d3..480ef28432 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -16,7 +16,6 @@ from ayon_core.pipeline.plugin_discover import ( register_plugin_path, ) from ayon_core.pipeline.staging_dir import StagingDir, get_staging_dir_info -from ayon_core.settings import get_project_settings from .constants import DEFAULT_VARIANT_VALUE from .product_name import get_product_name From 862049d995087d163ac02cb2c2538dcc53dffe38 Mon Sep 17 00:00:00 2001 From: timsergeeff <38128238+timsergeeff@users.noreply.github.com> Date: Fri, 10 Oct 2025 13:00:09 +0300 Subject: [PATCH 13/90] Refactor color conversion logic in transcoding.py --- client/ayon_core/lib/transcoding.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 127bd3bac4..12a64c7e06 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1170,6 +1170,14 @@ def oiio_color_convert( # Handle the different conversion cases # Source view and display are known if source_view and source_display: + color_convert_args = None + ocio_display_args = None + oiio_cmd.extend([ + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view + ]) + if target_colorspace: # This is a two-step conversion process since there's no direct # display/view to colorspace command @@ -1179,22 +1187,28 @@ def oiio_color_convert( elif source_display != target_display or source_view != target_view: # Complete display/view pair conversion # - go through a reference space - color_convert_args = (target_display, target_view) + ocio_display_args = (target_display, target_view) else: color_convert_args = None + ocio_display_args = None logger.debug( "Source and target display/view pairs are identical." " No color conversion needed." ) + if color_convert_args: + # Use colorconvert for colorspace target oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view, "--colorconvert:subimages=0", *color_convert_args ]) + elif ocio_display_args: + # Use ociodisplay for display/view target + oiio_cmd.extend([ + "--ociodisplay:subimages=0", + *ocio_display_args + ]) elif target_colorspace: # Standard color space to color space conversion From 0db3f67eb3bdb04b84b8818d52971f251af1cc8e Mon Sep 17 00:00:00 2001 From: timsergeeff <38128238+timsergeeff@users.noreply.github.com> Date: Fri, 10 Oct 2025 15:06:51 +0300 Subject: [PATCH 14/90] Remove unnecessary blank lines in transcoding.py --- client/ayon_core/lib/transcoding.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 12a64c7e06..9216c88ed2 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1196,7 +1196,6 @@ def oiio_color_convert( " No color conversion needed." ) - if color_convert_args: # Use colorconvert for colorspace target oiio_cmd.extend([ @@ -1232,7 +1231,6 @@ def oiio_color_convert( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) - def split_cmd_args(in_args): """Makes sure all entered arguments are separated in individual items. From 7ef330c3f4fb0e59af9d2a58a2470a2230bd23c2 Mon Sep 17 00:00:00 2001 From: timsergeeff <38128238+timsergeeff@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:55:12 +0300 Subject: [PATCH 15/90] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 9216c88ed2..fcf7fdece2 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1173,10 +1173,10 @@ def oiio_color_convert( color_convert_args = None ocio_display_args = None oiio_cmd.extend([ - "--ociodisplay:inverse=1:subimages=0", - source_display, - source_view - ]) + "--ociodisplay:inverse=1:subimages=0", + source_display, + source_view, + ]) if target_colorspace: # This is a two-step conversion process since there's no direct From 2541f8909e6625e710e376e4dc4c10a21a7db082 Mon Sep 17 00:00:00 2001 From: timsergeeff <38128238+timsergeeff@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:55:18 +0300 Subject: [PATCH 16/90] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index fcf7fdece2..70a8f26cf2 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1189,8 +1189,6 @@ def oiio_color_convert( # - go through a reference space ocio_display_args = (target_display, target_view) else: - color_convert_args = None - ocio_display_args = None logger.debug( "Source and target display/view pairs are identical." " No color conversion needed." From aabd9f7f505ddd2972bf2fa9afd5d28318b36299 Mon Sep 17 00:00:00 2001 From: timsergeeff <38128238+timsergeeff@users.noreply.github.com> Date: Fri, 10 Oct 2025 21:55:23 +0300 Subject: [PATCH 17/90] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 70a8f26cf2..37fcb59ab3 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1229,6 +1229,7 @@ def oiio_color_convert( logger.debug("Conversion command: {}".format(" ".join(oiio_cmd))) run_subprocess(oiio_cmd, logger=logger) + def split_cmd_args(in_args): """Makes sure all entered arguments are separated in individual items. From f147d28c528f67635c9aafe1d03fa7f8a51cbafd Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 17 Oct 2025 17:36:47 +0200 Subject: [PATCH 18/90] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20add=20tests=20for=20?= =?UTF-8?q?product=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipeline/create/test_product_name.py | 372 ++++++++++++++++++ 1 file changed, 372 insertions(+) create mode 100644 tests/client/ayon_core/pipeline/create/test_product_name.py diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py new file mode 100644 index 0000000000..b4507e39f1 --- /dev/null +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -0,0 +1,372 @@ +"""Tests for product_name helpers.""" +import pytest +from unittest.mock import patch + +from ayon_core.pipeline.create.product_name import ( + get_product_name_template, + get_product_name, +) +from ayon_core.pipeline.create.constants import DEFAULT_PRODUCT_TEMPLATE +from ayon_core.pipeline.create.exceptions import ( + TaskNotSetError, + TemplateFillError, +) + + +class TestGetProductNameTemplate: + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_matching_profile_with_replacements( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + """Matching profile applies legacy replacement tokens.""" + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + # The function should replace {task}/{family}/{asset} variants + mock_filter_profiles.return_value = { + "template": ("{task}-{Task}-{TASK}-{family}-{Family}" + "-{FAMILY}-{asset}-{Asset}-{ASSET}") + } + mock_is_supported.return_value = False + + result = get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + ) + assert result == ( + "{task[name]}-{Task[name]}-{TASK[NAME]}-" + "{product[type]}-{Product[type]}-{PRODUCT[TYPE]}-" + "{folder[name]}-{Folder[name]}-{FOLDER[NAME]}" + ) + + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_no_matching_profile_uses_default( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = False + + assert ( + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + ) + == DEFAULT_PRODUCT_TEMPLATE + ) + + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_custom_default_template_used( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = False + + custom_default = "{variant}_{family}" + assert ( + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + default_template=custom_default, + ) + == custom_default + ) + + @patch("ayon_core.pipeline.create.product_name.warn") + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_product_base_type_warns_when_supported_and_missing( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + mock_warn, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = True + + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + ) + mock_warn.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_project_settings") + @patch("ayon_core.pipeline.create.product_name.filter_profiles") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + def test_product_base_type_added_to_filtering_when_provided( + self, + mock_is_supported, + mock_filter_profiles, + mock_get_settings, + ): + mock_get_settings.return_value = { + "core": {"tools": {"creator": {"product_name_profiles": []}}} + } + mock_filter_profiles.return_value = None + mock_is_supported.return_value = True + + get_product_name_template( + project_name="proj", + product_type="model", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_base_type="asset", + ) + args, kwargs = mock_filter_profiles.call_args + # args[1] is filtering_criteria + assert args[1]["product_base_types"] == "asset" + + +class TestGetProductName: + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_empty_product_type_returns_empty( + self, mock_prepare, mock_format, mock_get_tmpl + ): + assert ( + get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="", + variant="Main", + ) + == "" + ) + mock_get_tmpl.assert_not_called() + mock_format.assert_not_called() + mock_prepare.assert_not_called() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_happy_path( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{task[name]}_{product[type]}_{variant}" + mock_prepare.return_value = { + "task": {"name": "modeling"}, + "product": {"type": "model"}, + "variant": "Main", + "family": "model", + } + mock_format.return_value = "modeling_model_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + assert result == "modeling_model_Main" + mock_get_tmpl.assert_called_once() + mock_prepare.assert_called_once() + mock_format.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_product_name_with_base_type( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{task[name]}_{product[basetype]}_{variant}" + mock_prepare.return_value = { + "task": {"name": "modeling"}, + "product": {"type": "model"}, + "variant": "Main", + "family": "model", + } + mock_format.return_value = "modeling_modelBase_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + product_base_type="modelBase", + variant="Main", + ) + assert result == "modeling_modelBase_Main" + mock_get_tmpl.assert_called_once() + mock_prepare.assert_called_once() + mock_format.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + def test_task_required_but_missing_raises(self, mock_get_tmpl): + mock_get_tmpl.return_value = "{task[name]}_{variant}" + with pytest.raises(TaskNotSetError): + get_product_name( + project_name="proj", + task_name="", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name.ayon_api.get_project") + @patch("ayon_core.pipeline.create.product_name.StringTemplate." + "format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_task_short_name_is_used( + self, mock_prepare, mock_format, mock_get_project, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{task[short]}_{variant}" + mock_get_project.return_value = { + "taskTypes": [{"name": "Modeling", "shortName": "mdl"}] + } + mock_prepare.return_value = {"task": {"short": "mdl"}, "variant": "Main"} + mock_format.return_value = "mdl_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + assert result == "mdl_Main" + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name.StringTemplate." + "format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_template_fill_error_translated( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{missing_key}_{variant}" + mock_prepare.return_value = {"variant": "Main"} + mock_format.side_effect = KeyError("missing_key") + with pytest.raises(TemplateFillError): + get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + + @patch("ayon_core.pipeline.create.product_name.warn") + @patch("ayon_core.pipeline.create.product_name." + "is_product_base_type_supported") + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_warns_when_template_needs_base_type_but_missing( + self, + mock_prepare, + mock_format, + mock_get_tmpl, + mock_is_supported, + mock_warn, + ): + mock_get_tmpl.return_value = "{product[basetype]}_{variant}" + mock_is_supported.return_value = True + mock_prepare.return_value = { + "product": {"type": "model"}, + "variant": "Main", + "family": "model", + } + mock_format.return_value = "asset_Main" + + _ = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + ) + mock_warn.assert_called_once() + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + @patch("ayon_core.pipeline.create.product_name." + "StringTemplate.format_strict_template") + @patch("ayon_core.pipeline.create.product_name.prepare_template_data") + def test_dynamic_data_overrides_defaults( + self, mock_prepare, mock_format, mock_get_tmpl + ): + mock_get_tmpl.return_value = "{custom}_{variant}" + mock_prepare.return_value = {"custom": "overridden", "variant": "Main"} + mock_format.return_value = "overridden_Main" + + result = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + dynamic_data={"custom": "overridden"}, + ) + assert result == "overridden_Main" + + @patch("ayon_core.pipeline.create.product_name.get_product_name_template") + def test_product_type_filter_is_used(self, mock_get_tmpl): + mock_get_tmpl.return_value = DEFAULT_PRODUCT_TEMPLATE + _ = get_product_name( + project_name="proj", + task_name="modeling", + task_type="Modeling", + host_name="maya", + product_type="model", + variant="Main", + product_type_filter="look", + ) + args, kwargs = mock_get_tmpl.call_args + assert kwargs["product_type"] == "look" From 0ca2d25ef651775b7f27b4d6cacc18a40c41bdb1 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 17 Oct 2025 17:41:50 +0200 Subject: [PATCH 19/90] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20fix=20linting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ayon_core/pipeline/create/test_product_name.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py index b4507e39f1..a8a8566aa8 100644 --- a/tests/client/ayon_core/pipeline/create/test_product_name.py +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -219,7 +219,9 @@ class TestGetProductName: def test_product_name_with_base_type( self, mock_prepare, mock_format, mock_get_tmpl ): - mock_get_tmpl.return_value = "{task[name]}_{product[basetype]}_{variant}" + mock_get_tmpl.return_value = ( + "{task[name]}_{product[basetype]}_{variant}" + ) mock_prepare.return_value = { "task": {"name": "modeling"}, "product": {"type": "model"}, @@ -267,7 +269,12 @@ class TestGetProductName: mock_get_project.return_value = { "taskTypes": [{"name": "Modeling", "shortName": "mdl"}] } - mock_prepare.return_value = {"task": {"short": "mdl"}, "variant": "Main"} + mock_prepare.return_value = { + "task": { + "short": "mdl" + }, + "variant": "Main" + } mock_format.return_value = "mdl_Main" result = get_product_name( From 2fe89c4b4619d01b53f0bcdbc5ef2c20b5d05c5c Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Nov 2025 22:09:25 +0800 Subject: [PATCH 20/90] add substance painter as host and adjust some instance data so that it can be used to review for image product --- client/ayon_core/plugins/publish/extract_review.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 580aa27eef..665d031d5a 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -163,7 +163,8 @@ class ExtractReview(pyblish.api.InstancePlugin): "flame", "unreal", "batchdelivery", - "photoshop" + "photoshop", + "substancepainter", ] settings_category = "core" @@ -571,7 +572,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # review output files "timecode": frame_to_timecode( frame=temp_data.frame_start_handle, - fps=float(instance.data["fps"]) + fps=float(instance.data.get("fps", 25.0)) ) }) @@ -664,8 +665,8 @@ class ExtractReview(pyblish.api.InstancePlugin): with values may be added. """ - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] + frame_start = instance.data.get("frameStart", 1) + frame_end = instance.data.get("frameEnd", 1) # Try to get handles from instance handle_start = instance.data.get("handleStart") @@ -725,7 +726,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"])[1].replace(".", "") return TempData( - fps=float(instance.data["fps"]), + fps=float(instance.data.get("fps", 25.0)), frame_start=frame_start, frame_end=frame_end, handle_start=handle_start, From cfed4afaaf7e22741b463fe77fb2eb3affaf7156 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Nov 2025 15:30:33 +0800 Subject: [PATCH 21/90] use the frame range from context data if it cannot find one --- client/ayon_core/plugins/publish/extract_review.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 665d031d5a..e519a4a97d 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -572,7 +572,9 @@ class ExtractReview(pyblish.api.InstancePlugin): # review output files "timecode": frame_to_timecode( frame=temp_data.frame_start_handle, - fps=float(instance.data.get("fps", 25.0)) + fps=float(instance.data.get( + "fps", instance.context.data["fps"] + )) ) }) @@ -665,8 +667,12 @@ class ExtractReview(pyblish.api.InstancePlugin): with values may be added. """ - frame_start = instance.data.get("frameStart", 1) - frame_end = instance.data.get("frameEnd", 1) + frame_start = instance.data.get( + "frameStart", instance.context.data["frameStart"] + ) + frame_end = instance.data.get( + "frameEnd", instance.context.data["frameEnd"] + ) # Try to get handles from instance handle_start = instance.data.get("handleStart") @@ -726,7 +732,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"])[1].replace(".", "") return TempData( - fps=float(instance.data.get("fps", 25.0)), + fps=float(instance.data.get("fps", instance.context.data["fps"])), frame_start=frame_start, frame_end=frame_end, handle_start=handle_start, From 5ab274aa503cecc07a7289175b5c61f755c0aede Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Nov 2025 17:47:49 +0800 Subject: [PATCH 22/90] restore the instance data and adjust them into textureset collector in substance instead --- client/ayon_core/plugins/publish/extract_review.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index e519a4a97d..3f2a0dcd3e 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -572,9 +572,7 @@ class ExtractReview(pyblish.api.InstancePlugin): # review output files "timecode": frame_to_timecode( frame=temp_data.frame_start_handle, - fps=float(instance.data.get( - "fps", instance.context.data["fps"] - )) + fps=float(instance.data["fps"]) ) }) @@ -667,12 +665,8 @@ class ExtractReview(pyblish.api.InstancePlugin): with values may be added. """ - frame_start = instance.data.get( - "frameStart", instance.context.data["frameStart"] - ) - frame_end = instance.data.get( - "frameEnd", instance.context.data["frameEnd"] - ) + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] # Try to get handles from instance handle_start = instance.data.get("handleStart") @@ -732,7 +726,7 @@ class ExtractReview(pyblish.api.InstancePlugin): ext = os.path.splitext(repre["files"])[1].replace(".", "") return TempData( - fps=float(instance.data.get("fps", instance.context.data["fps"])), + fps=float(instance.data["fps"]), frame_start=frame_start, frame_end=frame_end, handle_start=handle_start, From c0fd2aa8c57b5ef17cfe38245f74f0b889243e51 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Nov 2025 18:10:52 +0800 Subject: [PATCH 23/90] add additional default settings into ExtractReview for substance painter --- server/settings/publish_plugins.py | 99 ++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..311b4672cf 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -1448,6 +1448,105 @@ DEFAULT_PUBLISH_VALUES = { "fill_missing_frames": "closest_existing" } ] + }, + { + "product_types": [], + "hosts": ["substancepainter"], + "task_types": [], + "outputs": [ + { + "name": "png", + "ext": "png", + "tags": [ + "ftrackreview", + "kitsureview", + "webreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [], + "output": [] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "single_frame" + }, + "overscan_crop": "", + # "overscan_color": [0, 0, 0], + "overscan_color": [0, 0, 0, 0.0], + "width": 1920, + "height": 1080, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + }, + "fill_missing_frames": "only_rendered" + }, + { + "name": "h264", + "ext": "mp4", + "tags": [ + "burnin", + "ftrackreview", + "kitsureview", + "webreview" + ], + "burnins": [], + "ffmpeg_args": { + "video_filters": [], + "audio_filters": [], + "input": [ + "-apply_trc gamma22" + ], + "output": [ + "-pix_fmt yuv420p", + "-crf 18", + "-c:a aac", + "-b:a 192k", + "-g 1", + "-movflags faststart" + ] + }, + "filter": { + "families": [ + "render", + "review", + "ftrack" + ], + "product_names": [], + "custom_tags": [], + "single_frame_filter": "multi_frame" + }, + "overscan_crop": "", + # "overscan_color": [0, 0, 0], + "overscan_color": [0, 0, 0, 0.0], + "width": 0, + "height": 0, + "scale_pixel_aspect": True, + "bg_color": [0, 0, 0, 0.0], + "letter_box": { + "enabled": False, + "ratio": 0.0, + "fill_color": [0, 0, 0, 1.0], + "line_thickness": 0, + "line_color": [255, 0, 0, 1.0] + }, + "fill_missing_frames": "only_rendered" + } + ] } ] }, From 67d5422c94584a100a1d3818137c4e16e06465ab Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 8 Nov 2025 22:45:42 +0100 Subject: [PATCH 24/90] If folder name starts with a digit we now prefix it with `_` to avoid invalid USD data to be authored. --- client/ayon_core/pipeline/usdlib.py | 17 +++++++++++++++++ .../publish/extract_usd_layer_contributions.py | 5 +++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index 2ff98c5e45..095f6fdc57 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -684,3 +684,20 @@ def get_sdf_format_args(path): """Return SDF_FORMAT_ARGS parsed to `dict`""" _raw_path, data = Sdf.Layer.SplitIdentifier(path) return data + + +def get_standard_default_prim_name(folder_path: str) -> str: + """Return the AYON-specified default prim name for a folder path. + + This is used e.g. for the default prim in AYON USD Contribution workflows. + """ + folder_name: str = folder_path.rsplit("/", 1)[-1] + + # Prim names are not allowed to start with a digit in USD. Authoring them + # would mean generating essentially garbage data and may result in + # unexpected behavior in certain USD or DCC versions, like failure to + # refresh in usdview or crashes in Houdini 21. + if folder_name and folder_name[0].isdigit(): + folder_name = f"_{folder_name}" + + return folder_name diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 9db8c49a02..c73d1aa447 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -25,7 +25,8 @@ try: variant_nested_prim_path, setup_asset_layer, add_ordered_sublayer, - set_layer_defaults + set_layer_defaults, + get_standard_default_prim_name ) except ImportError: pass @@ -652,7 +653,7 @@ class ExtractUSDLayerContribution(publish.Extractor): sdf_layer = Sdf.Layer.OpenAsAnonymous(path) default_prim = sdf_layer.defaultPrim else: - default_prim = folder_path.rsplit("/", 1)[-1] # use folder name + default_prim = get_standard_default_prim_name(folder_path) sdf_layer = Sdf.Layer.CreateAnonymous() set_layer_defaults(sdf_layer, default_prim=default_prim) From 113d01ce9952762a7dafca4960f80d899bdbc395 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 8 Nov 2025 22:54:24 +0100 Subject: [PATCH 25/90] Add setting to always enforce the default prim value --- .../extract_usd_layer_contributions.py | 10 +++++++++ server/settings/publish_plugins.py | 22 ++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index c73d1aa447..ff1c4fec8a 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -641,6 +641,7 @@ class ExtractUSDLayerContribution(publish.Extractor): settings_category = "core" use_ayon_entity_uri = False + enforce_default_prim = False def process(self, instance): @@ -651,6 +652,15 @@ class ExtractUSDLayerContribution(publish.Extractor): path = get_last_publish(instance) if path and BUILD_INTO_LAST_VERSIONS: sdf_layer = Sdf.Layer.OpenAsAnonymous(path) + + # If enabled in settings, ignore any default prim specified on + # older publish versions and always publish with the AYON + # standard default prim + if self.enforce_default_prim: + sdf_layer.defaultPrim = get_standard_default_prim_name( + folder_path + ) + default_prim = sdf_layer.defaultPrim else: default_prim = get_standard_default_prim_name(folder_path) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..60bd933344 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -251,6 +251,19 @@ class AyonEntityURIModel(BaseSettingsModel): ) +class ExtractUSDLayerContributionModel(AyonEntityURIModel): + enforce_default_prim: bool = SettingsField( + title="Always set default prim to folder name.", + description=( + "When enabled ignore any default prim specified on older " + "published versions of a layer and always override it to the " + "AYON standard default prim. When disabled, preserve default prim " + "on the layer and then only the initial version would be setting " + "the AYON standard default prim." + ) + ) + + class PluginStateByHostModelProfile(BaseSettingsModel): _layout = "expanded" # Filtering @@ -1134,9 +1147,11 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=AyonEntityURIModel, title="Extract USD Asset Contribution", ) - ExtractUSDLayerContribution: AyonEntityURIModel = SettingsField( - default_factory=AyonEntityURIModel, - title="Extract USD Layer Contribution", + ExtractUSDLayerContribution: ExtractUSDLayerContributionModel = ( + SettingsField( + default_factory=ExtractUSDLayerContributionModel, + title="Extract USD Layer Contribution", + ) ) PreIntegrateThumbnails: PreIntegrateThumbnailsModel = SettingsField( default_factory=PreIntegrateThumbnailsModel, @@ -1526,6 +1541,7 @@ DEFAULT_PUBLISH_VALUES = { }, "ExtractUSDLayerContribution": { "use_ayon_entity_uri": False, + "enforce_default_prim": False, }, "PreIntegrateThumbnails": { "enabled": True, From e7896c66f3b513ae1f024259943ad4cf3f60fb09 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 9 Nov 2025 11:33:33 +0100 Subject: [PATCH 26/90] Update server/settings/publish_plugins.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- server/settings/publish_plugins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 60bd933344..e939a6518b 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -256,7 +256,7 @@ class ExtractUSDLayerContributionModel(AyonEntityURIModel): title="Always set default prim to folder name.", description=( "When enabled ignore any default prim specified on older " - "published versions of a layer and always override it to the " + "published versions of a layer and always override it to the " "AYON standard default prim. When disabled, preserve default prim " "on the layer and then only the initial version would be setting " "the AYON standard default prim." From 0dfaed53cba2a76f2463bc2fbf6c5ef585dd6dfa Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 11 Nov 2025 12:43:00 +0100 Subject: [PATCH 27/90] Fix setting display/view based on collected scene display/view if left empty in ExtractOIIOTranscode settings. `instance.data["sceneDisplay"]` and `instance.data["sceneView"]` are now intended to be set to describe the user's configured display/view inside the DCC and can still be used as fallback for the `ExtractOIIOTrancode` transcoding. For the time being the legacy `colorspaceDisplay` and `colorspaceView` instance.data keys will act as fallback for backwards compatibility to represent the scene display and view. Also see: https://github.com/ynput/ayon-core/issues/1430#issuecomment-3516459205 --- .../pipeline/farm/pyblish_functions.py | 4 +- .../publish/extract_color_transcode.py | 44 +++++++++---------- server/settings/publish_plugins.py | 4 +- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 2193e96cb1..45c9eb22dc 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -594,8 +594,8 @@ def create_instances_for_aov( additional_color_data = { "renderProducts": instance.data["renderProducts"], "colorspaceConfig": instance.data["colorspaceConfig"], - "display": instance.data["colorspaceDisplay"], - "view": instance.data["colorspaceView"] + "display": instance.data.get("sourceDisplay"), + "view": instance.data.get("sourceView") } # Get templated path from absolute config path. diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 1a2c85e597..b293bd29c3 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -87,15 +87,19 @@ class ExtractOIIOTranscode(publish.Extractor): profile_output_defs = profile["outputs"] new_representations = [] repres = instance.data["representations"] - for idx, repre in enumerate(list(repres)): - # target space, display and view might be defined upstream - # TODO: address https://github.com/ynput/ayon-core/pull/1268#discussion_r2156555474 - # Implement upstream logic to handle target_colorspace, - # target_display, target_view in other DCCs - target_colorspace = False - target_display = instance.data.get("colorspaceDisplay") - target_view = instance.data.get("colorspaceView") + scene_display = instance.data.get( + "sceneDisplay", + # Backward compatibility + instance.data.get("colorspaceDisplay") + ) + scene_view = instance.data.get( + "sceneView", + # Backward compatibility + instance.data.get("colorspaceView") + ) + + for idx, repre in enumerate(list(repres)): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): continue @@ -142,24 +146,18 @@ class ExtractOIIOTranscode(publish.Extractor): transcoding_type = output_def["transcoding_type"] - # NOTE: we use colorspace_data as the fallback values for - # the target colorspace. + # Set target colorspace/display/view based on transcoding type + target_colorspace = None + target_view = None + target_display = None if transcoding_type == "colorspace": - # TODO: Should we fallback to the colorspace - # (which used as source above) ? - # or should we compute the target colorspace from - # current view and display ? - target_colorspace = (output_def["colorspace"] or - colorspace_data.get("colorspace")) + target_colorspace = output_def["colorspace"] elif transcoding_type == "display_view": display_view = output_def["display_view"] - target_view = ( - display_view["view"] - or colorspace_data.get("view")) - target_display = ( - display_view["display"] - or colorspace_data.get("display") - ) + # If empty values are provided in output definition, + # fallback to scene display/view that is collected from DCC + target_view = display_view["view"] or scene_view + target_display = display_view["display"] or scene_display # both could be already collected by DCC, # but could be overwritten when transcoding diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..7311115966 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -443,7 +443,7 @@ class UseDisplayViewModel(BaseSettingsModel): title="Target Display", description=( "Display of the target transform. If left empty, the" - " source Display value will be used." + " scene Display value will be used." ) ) view: str = SettingsField( @@ -451,7 +451,7 @@ class UseDisplayViewModel(BaseSettingsModel): title="Target View", description=( "View of the target transform. If left empty, the" - " source View value will be used." + " scene View value will be used." ) ) From 80a95c19f143b4593d9ca610c2e5d55fc7feda56 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 13 Nov 2025 23:24:08 +0100 Subject: [PATCH 28/90] Pass on `sceneDisplay` (legacy `colorspaceDisplay`) and `sceneView` (legacy `colorspaceView`) to metadata JSON. Also pass on `sourceDisplay` and `sourceView` --- client/ayon_core/pipeline/farm/pyblish_functions.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index 45c9eb22dc..c220b75403 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -253,6 +253,19 @@ def create_skeleton_instance( "reuseLastVersion": data.get("reuseLastVersion", False), } + # Pass on the OCIO metadata of what the source display and view are + # so that the farm can correctly set up color management. + if "sceneDisplay" in data and "sceneView" in data: + instance_skeleton_data["sceneDisplay"] = data["sceneDisplay"] + instance_skeleton_data["sceneView"] = data["sceneView"] + elif "colorspaceDisplay" in data and "colorspaceView" in data: + # Backwards compatibility for sceneDisplay and sceneView + instance_skeleton_data["colorspaceDisplay"] = data["colorspaceDisplay"] + instance_skeleton_data["colorspaceView"] = data["colorspaceView"] + if "sourceDisplay" in data and "sourceView" in data: + instance_skeleton_data["sourceDisplay"] = data["sourceDisplay"] + instance_skeleton_data["sourceView"] = data["sourceView"] + if data.get("renderlayer"): instance_skeleton_data["renderlayer"] = data["renderlayer"] From aea231d64e6a07258ee106ec7043967630e4e21c Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 16 Nov 2025 22:20:57 +0100 Subject: [PATCH 29/90] Also prefix folder name with `_` for initializing the asset layer --- .../plugins/publish/extract_usd_layer_contributions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index ff1c4fec8a..4d8a8005f2 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -821,7 +821,7 @@ class ExtractUSDAssetContribution(publish.Extractor): folder_path = instance.data["folderPath"] product_name = instance.data["productName"] self.log.debug(f"Building asset: {folder_path} > {product_name}") - folder_name = folder_path.rsplit("/", 1)[-1] + asset_name = get_standard_default_prim_name(folder_path) # Contribute layers to asset # Use existing asset and add to it, or initialize a new asset layer @@ -840,7 +840,7 @@ class ExtractUSDAssetContribution(publish.Extractor): # the layer as either a default asset or shot structure. init_type = instance.data["contribution_target_product_init"] asset_layer, payload_layer = self.init_layer( - asset_name=folder_name, init_type=init_type + asset_name=asset_name, init_type=init_type ) # Author timeCodesPerSecond and framesPerSecond if the asset layer From 335f9cf21b7ccba2ca6674081c8b9eb6e11b9f3f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 14:39:27 +0100 Subject: [PATCH 30/90] Implement generic ExtractOIIOPostProcess plug-in. This can be used to take any image representation through `oiiotool` to process with settings-defined arguments, to e.g. resize an image, convert all layers to scanline, etc. --- .../publish/extract_oiio_postprocess.py | 322 ++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 client/ayon_core/plugins/publish/extract_oiio_postprocess.py diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py new file mode 100644 index 0000000000..6163eb98d2 --- /dev/null +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -0,0 +1,322 @@ +import os +import copy +import clique +import pyblish.api + +from ayon_core.pipeline import ( + publish, + get_temp_dir +) +from ayon_core.lib import ( + is_oiio_supported, + get_oiio_tool_args, + run_subprocess +) +from ayon_core.lib.profiles_filtering import filter_profiles + + +class ExtractOIIOPostProcess(publish.Extractor): + """Process representations through `oiiotool` with profile defined + settings so that e.g. color space conversions can be applied or images + could be converted to scanline, resized, etc. regardless of colorspace + data. + """ + + label = "OIIO Post Process" + order = pyblish.api.ExtractorOrder + 0.020 + + settings_category = "core" + + optional = True + + # Supported extensions + supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"} + + # Configurable by Settings + profiles = None + options = None + + def process(self, instance): + if not self.profiles: + self.log.debug("No profiles present for OIIO Post Process") + return + + if "representations" not in instance.data: + self.log.debug("No representations, skipping.") + return + + if not is_oiio_supported(): + self.log.warning("OIIO not supported, no transcoding possible.") + return + + profile, representations = self._get_profile( + instance + ) + if not profile: + return + + profile_output_defs = profile["outputs"] + new_representations = [] + for idx, repre in enumerate(list(instance.data["representations"])): + self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) + if not self._repre_is_valid(repre, profile): + continue + + # Get representation files to convert + if isinstance(repre["files"], list): + repre_files_to_convert = copy.deepcopy(repre["files"]) + else: + repre_files_to_convert = [repre["files"]] + + added_representations = False + added_review = False + + # Process each output definition + for output_def in profile_output_defs: + + # Local copy to avoid accidental mutable changes + files_to_convert = list(repre_files_to_convert) + + output_name = output_def["name"] + new_repre = copy.deepcopy(repre) + + original_staging_dir = new_repre["stagingDir"] + new_staging_dir = get_temp_dir( + project_name=instance.context.data["projectName"], + use_local_temp=True, + ) + new_repre["stagingDir"] = new_staging_dir + + output_extension = output_def["extension"] + output_extension = output_extension.replace('.', '') + self._rename_in_representation(new_repre, + files_to_convert, + output_name, + output_extension) + + sequence_files = self._translate_to_sequence(files_to_convert) + self.log.debug("Files to convert: {}".format(sequence_files)) + for file_name in sequence_files: + if isinstance(file_name, clique.Collection): + # Convert to filepath that can be directly converted + # by oiio like `frame.1001-1025%04d.exr` + file_name: str = file_name.format( + "{head}{range}{padding}{tail}" + ) + + self.log.debug("Transcoding file: `{}`".format(file_name)) + input_path = os.path.join(original_staging_dir, + file_name) + output_path = self._get_output_file_path(input_path, + new_staging_dir, + output_extension) + + # TODO: Support formatting with dynamic keys from the + # representation, like e.g. colorspace config, display, + # view, etc. + input_arguments: list[str] = output_def.get( + "input_arguments", [] + ) + output_arguments: list[str] = output_def.get( + "output_arguments", [] + ) + + # Prepare subprocess arguments + oiio_cmd = get_oiio_tool_args( + "oiiotool", + *input_arguments, + input_path, + *output_arguments, + "-o", + output_path + ) + + self.log.debug( + "Conversion command: {}".format(" ".join(oiio_cmd))) + run_subprocess(oiio_cmd, logger=self.log) + + # cleanup temporary transcoded files + for file_name in new_repre["files"]: + transcoded_file_path = os.path.join(new_staging_dir, + file_name) + instance.context.data["cleanupFullPaths"].append( + transcoded_file_path) + + custom_tags = output_def.get("custom_tags") + if custom_tags: + if new_repre.get("custom_tags") is None: + new_repre["custom_tags"] = [] + new_repre["custom_tags"].extend(custom_tags) + + # Add additional tags from output definition to representation + if new_repre.get("tags") is None: + new_repre["tags"] = [] + for tag in output_def["tags"]: + if tag not in new_repre["tags"]: + new_repre["tags"].append(tag) + + if tag == "review": + added_review = True + + # If there is only 1 file outputted then convert list to + # string, because that'll indicate that it is not a sequence. + if len(new_repre["files"]) == 1: + new_repre["files"] = new_repre["files"][0] + + # If the source representation has "review" tag, but it's not + # part of the output definition tags, then both the + # representations will be transcoded in ExtractReview and + # their outputs will clash in integration. + if "review" in repre.get("tags", []): + added_review = True + + new_representations.append(new_repre) + added_representations = True + + if added_representations: + self._mark_original_repre_for_deletion( + repre, profile, added_review + ) + + tags = repre.get("tags") or [] + if "delete" in tags and "thumbnail" not in tags: + instance.data["representations"].remove(repre) + + instance.data["representations"].extend(new_representations) + + def _rename_in_representation(self, new_repre, files_to_convert, + output_name, output_extension): + """Replace old extension with new one everywhere in representation. + + Args: + new_repre (dict) + files_to_convert (list): of filenames from repre["files"], + standardized to always list + output_name (str): key of output definition from Settings, + if "" token used, keep original repre name + output_extension (str): extension from output definition + """ + if output_name != "passthrough": + new_repre["name"] = output_name + if not output_extension: + return + + new_repre["ext"] = output_extension + new_repre["outputName"] = output_name + + renamed_files = [] + for file_name in files_to_convert: + file_name, _ = os.path.splitext(file_name) + file_name = '{}.{}'.format(file_name, + output_extension) + renamed_files.append(file_name) + new_repre["files"] = renamed_files + + def _translate_to_sequence(self, files_to_convert): + """Returns original list or a clique.Collection of a sequence. + + Uses clique to find frame sequence Collection. + If sequence not found, it returns original list. + + Args: + files_to_convert (list): list of file names + Returns: + list[str | clique.Collection]: List of filepaths or a list + of Collections (usually one, unless there are holes) + """ + pattern = [clique.PATTERNS["frames"]] + collections, _ = clique.assemble( + files_to_convert, patterns=pattern, + assume_padded_when_ambiguous=True) + if collections: + if len(collections) > 1: + raise ValueError( + "Too many collections {}".format(collections)) + + collection = collections[0] + # TODO: Technically oiiotool supports holes in the sequence as well + # using the dedicated --frames argument to specify the frames. + # We may want to use that too so conversions of sequences with + # holes will perform faster as well. + # Separate the collection so that we have no holes/gaps per + # collection. + return collection.separate() + + return files_to_convert + + def _get_output_file_path(self, input_path, output_dir, + output_extension): + """Create output file name path.""" + file_name = os.path.basename(input_path) + file_name, input_extension = os.path.splitext(file_name) + if not output_extension: + output_extension = input_extension.replace(".", "") + new_file_name = '{}.{}'.format(file_name, + output_extension) + return os.path.join(output_dir, new_file_name) + + def _get_profile(self, instance): + """Returns profile if it should process this instance.""" + host_name = instance.context.data["hostName"] + product_type = instance.data["productType"] + product_name = instance.data["productName"] + task_data = instance.data["anatomyData"].get("task", {}) + task_name = task_data.get("name") + task_type = task_data.get("type") + filtering_criteria = { + "hosts": host_name, + "product_types": product_type, + "product_names": product_name, + "task_names": task_name, + "task_types": task_type, + } + profile = filter_profiles(self.profiles, filtering_criteria, + logger=self.log) + + if not profile: + self.log.debug(( + "Skipped instance. None of profiles in presets are for" + " Host: \"{}\" | Product types: \"{}\" | Product names: \"{}\"" + " | Task name \"{}\" | Task type \"{}\"" + ).format( + host_name, product_type, product_name, task_name, task_type + )) + + return profile + + def _repre_is_valid(self, repre) -> bool: + """Validation if representation should be processed. + + Args: + repre (dict): Representation which should be checked. + + Returns: + bool: False if can't be processed else True. + """ + if repre.get("ext") not in self.supported_exts: + self.log.debug(( + "Representation '{}' has unsupported extension: '{}'. Skipped." + ).format(repre["name"], repre.get("ext"))) + return False + + if not repre.get("files"): + self.log.debug(( + "Representation '{}' has empty files. Skipped." + ).format(repre["name"])) + return False + + return True + + def _mark_original_repre_for_deletion(self, repre, profile, added_review): + """If new transcoded representation created, delete old.""" + if not repre.get("tags"): + repre["tags"] = [] + + delete_original = profile["delete_original"] + + if delete_original: + if "delete" not in repre["tags"]: + repre["tags"].append("delete") + + if added_review and "review" in repre["tags"]: + repre["tags"].remove("review") From a6ecea872efda602b2fe0b157924d893d6611192 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 17 Nov 2025 14:47:25 +0100 Subject: [PATCH 31/90] Add missing changes --- .../publish/extract_oiio_postprocess.py | 4 +- server/settings/publish_plugins.py | 111 ++++++++++++++++++ 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 6163eb98d2..2e93c68283 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -49,7 +49,7 @@ class ExtractOIIOPostProcess(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - profile, representations = self._get_profile( + profile = self._get_profile( instance ) if not profile: @@ -59,7 +59,7 @@ class ExtractOIIOPostProcess(publish.Extractor): new_representations = [] for idx, repre in enumerate(list(instance.data["representations"])): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) - if not self._repre_is_valid(repre, profile): + if not self._repre_is_valid(repre): continue # Get representation files to convert diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..173526e13f 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -565,12 +565,115 @@ class ExtractOIIOTranscodeProfileModel(BaseSettingsModel): class ExtractOIIOTranscodeModel(BaseSettingsModel): + """Color conversion transcoding using OIIO for images mostly aimed at + transcoding for reviewables (it'll process and output only RGBA channels). + """ enabled: bool = SettingsField(True) profiles: list[ExtractOIIOTranscodeProfileModel] = SettingsField( default_factory=list, title="Profiles" ) +class ExtractOIIOPostProcessOutputModel(BaseSettingsModel): + _layout = "expanded" + name: str = SettingsField( + "", + title="Name", + description="Output name (no space)", + regex=r"[a-zA-Z0-9_]([a-zA-Z0-9_\.\-]*[a-zA-Z0-9_])?$", + ) + extension: str = SettingsField( + "", + title="Extension", + description=( + "Target extension. If left empty, original" + " extension is used." + ), + ) + input_arguments: list[str] = SettingsField( + default_factory=list, + title="Input arguments", + description="Arguments passed prior to the input file argument.", + ) + output_arguments: list[str] = SettingsField( + default_factory=list, + title="Output arguments", + description="Arguments passed prior to the -o argument.", + ) + tags: list[str] = SettingsField( + default_factory=list, + title="Tags", + description=( + "Additional tags that will be added to the created representation." + "\nAdd *review* tag to create review from the transcoded" + " representation instead of the original." + ) + ) + custom_tags: list[str] = SettingsField( + default_factory=list, + title="Custom Tags", + description=( + "Additional custom tags that will be added" + " to the created representation." + ) + ) + + +class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): + product_types: list[str] = SettingsField( + default_factory=list, + title="Product types" + ) + hosts: list[str] = SettingsField( + default_factory=list, + title="Host names" + ) + task_types: list[str] = SettingsField( + default_factory=list, + title="Task types", + enum_resolver=task_types_enum + ) + task_names: list[str] = SettingsField( + default_factory=list, + title="Task names" + ) + product_names: list[str] = SettingsField( + default_factory=list, + title="Product names" + ) + delete_original: bool = SettingsField( + True, + title="Delete Original Representation", + description=( + "Choose to preserve or remove the original representation.\n" + "Keep in mind that if the transcoded representation includes" + " a `review` tag, it will take precedence over" + " the original for creating reviews." + ), + ) + outputs: list[ExtractOIIOPostProcessOutputModel] = SettingsField( + default_factory=list, + title="Output Definitions", + ) + + @validator("outputs") + def validate_unique_outputs(cls, value): + ensure_unique_names(value) + return value + + +class ExtractOIIOPostProcessModel(BaseSettingsModel): + """Process representation images with `oiiotool` on publish. + + This could be used to convert images to different formats, convert to + scanline images or flatten deep images. + """ + enabled: bool = SettingsField(True) + profiles: list[ExtractOIIOPostProcessProfileModel] = SettingsField( + default_factory=list, title="Profiles" + ) + + # --- [START] Extract Review --- class ExtractReviewFFmpegModel(BaseSettingsModel): video_filters: list[str] = SettingsField( @@ -1122,6 +1225,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=ExtractOIIOTranscodeModel, title="Extract OIIO Transcode" ) + ExtractOIIOPostProcess: ExtractOIIOPostProcessModel = SettingsField( + default_factory=ExtractOIIOPostProcessModel, + title="Extract OIIO Post Process" + ) ExtractReview: ExtractReviewModel = SettingsField( default_factory=ExtractReviewModel, title="Extract Review" @@ -1347,6 +1454,10 @@ DEFAULT_PUBLISH_VALUES = { "enabled": True, "profiles": [] }, + "ExtractOIIOPostProcess": { + "enabled": True, + "profiles": [] + }, "ExtractReview": { "enabled": True, "profiles": [ From 04527b00616908377547a6c1a71a88c1a2db7f76 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 21 Nov 2025 19:06:36 +0100 Subject: [PATCH 32/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20change=20usage=20of?= =?UTF-8?q?=20product=5Fbase=5Ftypes=20in=20plugins?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipeline/create/creator_plugins.py | 32 ++++++++----------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 480ef28432..93dd763ed9 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -1,4 +1,6 @@ """Creator plugins for the create process.""" +from __future__ import annotations + import collections import copy import os @@ -7,7 +9,6 @@ from typing import TYPE_CHECKING, Any, Dict, Optional from warnings import warn from ayon_core.lib import Logger, get_version_from_path -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline.plugin_discover import ( deregister_plugin, deregister_plugin_path, @@ -274,7 +275,7 @@ class BaseCreator(ABC): # - those may be potential dangerous typos in settings if not hasattr(self, key): self.log.debug( - "Applying settings to unknown attribute '%' on '%'.", + "Applying settings to unknown attribute '%s' on '%s'.", key, cls_name ) setattr(self, key, value) @@ -293,11 +294,9 @@ class BaseCreator(ABC): Default implementation returns plugin's product type. """ - identifier = self.product_type - if is_product_base_type_supported(): - identifier = self.product_base_type - if self.product_type: - identifier = f"{identifier}.{self.product_type}" + identifier = self.product_base_type + if not identifier: + identifier = self.product_type return identifier @property @@ -399,19 +398,14 @@ class BaseCreator(ABC): if product_type is None: product_type = self.product_type - if ( - is_product_base_type_supported() - and not product_base_type - and not self.product_base_type - ): + if not product_base_type and not self.product_base_type: warn( f"Creator {self.identifier} does not support " "product base type. This will be required in future.", DeprecationWarning, stacklevel=2, ) - else: - product_base_type = self.product_base_type + product_base_type = product_type instance = CreatedInstance( product_type=product_type, @@ -534,7 +528,6 @@ class BaseCreator(ABC): host_name: Optional[str] = None, instance: Optional[CreatedInstance] = None, project_entity: Optional[dict[str, Any]] = None, - product_base_type: Optional[str] = None, ) -> str: """Return product name for passed context. @@ -552,11 +545,11 @@ class BaseCreator(ABC): for which is product name updated. Passed only on product name update. project_entity (Optional[dict[str, Any]]): Project entity. - product_base_type (Optional[str]): Product base type. """ - if is_product_base_type_supported() and (instance and hasattr(instance, "product_base_type")): # noqa: E501 - product_base_type = instance.product_base_type + product_base_type = None + if hasattr(self, "product_base_type"): # noqa: E501 + product_base_type = self.product_base_type if host_name is None: host_name = self.create_context.host_name @@ -589,7 +582,8 @@ class BaseCreator(ABC): dynamic_data=dynamic_data, project_settings=self.project_settings, project_entity=project_entity, - product_base_type=product_base_type + # until we make product_base_type mandatory + product_base_type=self.product_base_type ) def get_instance_attr_defs(self): From 73dfff9191e4ba5d159d2d495c71950d60389236 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 22 Nov 2025 13:28:08 +0100 Subject: [PATCH 33/90] Fix call to `get_representation_path_by_names` --- .../plugins/publish/extract_usd_layer_contributions.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 4d8a8005f2..ec6b2b9792 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -177,7 +177,10 @@ def get_instance_uri_path( # If for whatever reason we were unable to retrieve from the context # then get the path from an existing database entry - path = get_representation_path_by_names(**query) + path = get_representation_path_by_names( + anatomy=context.data["anatomy"], + **names + ) # Ensure `None` for now is also a string path = str(path) From 70bf746c7acac36d8cf3670843b862cf08d82537 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 22 Nov 2025 13:29:49 +0100 Subject: [PATCH 34/90] Fail clearly if the path can't be resolved instead of setting path to "None" --- .../plugins/publish/extract_usd_layer_contributions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index ec6b2b9792..d5c5aca0f2 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -181,6 +181,8 @@ def get_instance_uri_path( anatomy=context.data["anatomy"], **names ) + if not path: + raise RuntimeError(f"Unable to resolve publish path for: {names}") # Ensure `None` for now is also a string path = str(path) From 58432ff4dd3aad7b879e72a6dcf158dce381d874 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Nov 2025 23:55:12 +0100 Subject: [PATCH 35/90] Re-show 'initialize as' attribute for USD publish so it's clear what is going on with the initial layer. --- .../plugins/publish/extract_usd_layer_contributions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 9db8c49a02..4dec4d8b9b 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -494,7 +494,7 @@ class CollectUSDLayerContributions(pyblish.api.InstancePlugin, "asset" if profile.get("contribution_target_product") == "usdAsset" else "shot") - init_as_visible = False + init_as_visible = True # Attributes logic publish_attributes = instance["publish_attributes"].get( @@ -828,6 +828,7 @@ class ExtractUSDAssetContribution(publish.Extractor): # If no existing publish of this product exists then we initialize # the layer as either a default asset or shot structure. init_type = instance.data["contribution_target_product_init"] + self.log.debug("Initializing layer as type: %s", init_type) asset_layer, payload_layer = self.init_layer( asset_name=folder_name, init_type=init_type ) From 2aa7e46c9c94e5224c74c043971749f9b3c8f671 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 25 Nov 2025 23:58:09 +0100 Subject: [PATCH 36/90] Cosmetic type hints --- .../plugins/publish/extract_usd_layer_contributions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py index 4dec4d8b9b..d73a417f16 100644 --- a/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py +++ b/client/ayon_core/plugins/publish/extract_usd_layer_contributions.py @@ -910,7 +910,7 @@ class ExtractUSDAssetContribution(publish.Extractor): payload_layer.Export(payload_path, args={"format": "usda"}) self.add_relative_file(instance, payload_path) - def init_layer(self, asset_name, init_type): + def init_layer(self, asset_name: str, init_type: str): """Initialize layer if no previous version exists""" if init_type == "asset": From 344f91c983256d5a29641c5e809761ab39b60f64 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 26 Nov 2025 00:38:37 +0100 Subject: [PATCH 37/90] Make settings profiles more granular for OIIO post process --- .../publish/extract_oiio_postprocess.py | 46 +++++++++++++------ server/settings/publish_plugins.py | 10 ++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 2e93c68283..610f464989 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -1,3 +1,5 @@ +from __future__ import annotations +from typing import Any, Optional import os import copy import clique @@ -49,19 +51,21 @@ class ExtractOIIOPostProcess(publish.Extractor): self.log.warning("OIIO not supported, no transcoding possible.") return - profile = self._get_profile( - instance - ) - if not profile: - return - - profile_output_defs = profile["outputs"] new_representations = [] for idx, repre in enumerate(list(instance.data["representations"])): self.log.debug("repre ({}): `{}`".format(idx + 1, repre["name"])) if not self._repre_is_valid(repre): continue + # We check profile per representation name and extension because + # it's included in the profile check. As such, an instance may have + # a different profile applied per representation. + profile = self._get_profile( + instance + ) + if not profile: + continue + # Get representation files to convert if isinstance(repre["files"], list): repre_files_to_convert = copy.deepcopy(repre["files"]) @@ -72,7 +76,7 @@ class ExtractOIIOPostProcess(publish.Extractor): added_review = False # Process each output definition - for output_def in profile_output_defs: + for output_def in profile["outputs"]: # Local copy to avoid accidental mutable changes files_to_convert = list(repre_files_to_convert) @@ -255,7 +259,7 @@ class ExtractOIIOPostProcess(publish.Extractor): output_extension) return os.path.join(output_dir, new_file_name) - def _get_profile(self, instance): + def _get_profile(self, instance: pyblish.api.Instance, repre: dict) -> Optional[dict[str, Any]]: """Returns profile if it should process this instance.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] @@ -263,24 +267,30 @@ class ExtractOIIOPostProcess(publish.Extractor): task_data = instance.data["anatomyData"].get("task", {}) task_name = task_data.get("name") task_type = task_data.get("type") + repre_name: str = repre["name"] + repre_ext: str = repre["ext"] filtering_criteria = { "hosts": host_name, "product_types": product_type, "product_names": product_name, "task_names": task_name, "task_types": task_type, + "representation_names": repre_name, + "representation_exts": repre_ext, } profile = filter_profiles(self.profiles, filtering_criteria, logger=self.log) if not profile: - self.log.debug(( + self.log.debug( "Skipped instance. None of profiles in presets are for" - " Host: \"{}\" | Product types: \"{}\" | Product names: \"{}\"" - " | Task name \"{}\" | Task type \"{}\"" - ).format( - host_name, product_type, product_name, task_name, task_type - )) + f" Host: \"{host_name}\" |" + f" Product types: \"{product_type}\" |" + f" Product names: \"{product_name}\" |" + f" Task name \"{task_name}\" |" + f" Task type \"{task_type}\" |" + f" Representation: \"{repre_name}\" (.{repre_ext})" + ) return profile @@ -305,6 +315,12 @@ class ExtractOIIOPostProcess(publish.Extractor): ).format(repre["name"])) return False + if "delete" in repre.get("tags", []): + self.log.debug(( + "Representation '{}' has 'delete' tag. Skipped." + ).format(repre["name"])) + return False + return True def _mark_original_repre_for_deletion(self, repre, profile, added_review): diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 173526e13f..2c133ddbbf 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -621,6 +621,7 @@ class ExtractOIIOPostProcessOutputModel(BaseSettingsModel): class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): product_types: list[str] = SettingsField( + section="Profile", default_factory=list, title="Product types" ) @@ -641,6 +642,14 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): default_factory=list, title="Product names" ) + representation_names: list[str] = SettingsField( + default_factory=list, + title="Representation names", + ) + representation_exts: list[str] = SettingsField( + default_factory=list, + title="Representation extensions", + ) delete_original: bool = SettingsField( True, title="Delete Original Representation", @@ -650,6 +659,7 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): " a `review` tag, it will take precedence over" " the original for creating reviews." ), + section="Conversion Outputs", ) outputs: list[ExtractOIIOPostProcessOutputModel] = SettingsField( default_factory=list, From 4f332766f0ccc9071d6f189abce8ed9fe67e9e6a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 26 Nov 2025 10:22:17 +0100 Subject: [PATCH 38/90] Use `IMAGE_EXTENSIONS` --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 610f464989..33384e0185 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -14,6 +14,7 @@ from ayon_core.lib import ( get_oiio_tool_args, run_subprocess ) +from ayon_core.lib.transcoding import IMAGE_EXTENSIONS from ayon_core.lib.profiles_filtering import filter_profiles @@ -32,7 +33,7 @@ class ExtractOIIOPostProcess(publish.Extractor): optional = True # Supported extensions - supported_exts = {"exr", "jpg", "jpeg", "png", "dpx"} + supported_exts = {ext.lstrip(".") for ext in IMAGE_EXTENSIONS} # Configurable by Settings profiles = None From 2cf392633e24b4465e846dbd534bd7461730da44 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 14:08:50 +0100 Subject: [PATCH 39/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20unnecessary?= =?UTF-8?q?=20checks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ayon_core/pipeline/create/product_name.py | 47 ++++++++++--------- 1 file changed, 26 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ab7de0c9e8..f1076e51b3 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -10,7 +10,6 @@ from ayon_core.lib import ( filter_profiles, prepare_template_data, ) -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.settings import get_project_settings from .constants import DEFAULT_PRODUCT_TEMPLATE @@ -36,10 +35,10 @@ def get_product_name_template( 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 (Optional, str): Default template which is used if + 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. product_base_type (Optional[str]): Base type of product. @@ -58,14 +57,16 @@ def get_product_name_template( "task_types": task_type } - if is_product_base_type_supported(): - if product_base_type: - filtering_criteria["product_base_types"] = product_base_type - else: - warn( - "Product base type is not provided, please update your" - "creation code to include it. It will be required in " - "the future.", DeprecationWarning, stacklevel=2) + if not product_base_type: + warn( + "Product base type is not provided, please update your" + "creation code to include it. It will be required in " + "the future.", + DeprecationWarning, + stacklevel=2 + ) + filtering_criteria["product_base_types"] = product_base_type + matching_profile = filter_profiles(profiles, filtering_criteria) template = None @@ -192,16 +193,20 @@ def get_product_name( # look what we have to do to make mypy happy. We should stop using # those undefined dict based types. - product: dict[str, str] = {"type": product_type} - if is_product_base_type_supported(): - if product_base_type: - product["baseType"] = product_base_type - elif "{product[basetype]}" in template.lower(): - warn( - "You have Product base type in product name template," - "but it is not provided by the creator, please update your" - "creation code to include it. It will be required in " - "the future.", DeprecationWarning, stacklevel=2) + product: dict[str, str] = { + "type": product_type, + "baseType": product_base_type + } + if not product_base_type and "{product[basetype]}" in template.lower(): + product["baseType"] = product_type + warn( + "You have Product base type in product name template, " + "but it is not provided by the creator, please update your " + "creation code to include it. It will be required in " + "the future.", + DeprecationWarning, + stacklevel=2) + fill_pairs: dict[str, Union[str, dict[str, str]]] = { "variant": variant, "family": product_type, From 05547c752ee788ec42fcf6c0b3235fc6f981353a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 14:27:22 +0100 Subject: [PATCH 40/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20remove=20the=20check?= =?UTF-8?q?=20for=20product=20base=20type=20support=20-=20publisher=20mode?= =?UTF-8?q?l?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/tools/publisher/models/create.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index ed3fd04d5c..86f0cd2d07 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -35,7 +35,6 @@ from ayon_core.pipeline.create import ( ConvertorsOperationFailed, ConvertorItem, ) -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.tools.publisher.abstract import ( AbstractPublisherBackend, @@ -666,18 +665,14 @@ class CreateModel: kwargs = { "instance": instance, "project_entity": project_entity, + "product_base_type": creator.product_base_type, } - - if is_product_base_type_supported() and hasattr(creator, "product_base_type"): # noqa: E501 - kwargs["product_base_type"] = creator.product_base_type - # 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") - kwargs.pop("product_base_type") return creator.get_product_name(*args, **kwargs) def create( From b967f8f818b74e15ce62e06374c6213d721ecf1f Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:01:25 +0100 Subject: [PATCH 41/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20consolidate=20warnin?= =?UTF-8?q?ings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/context.py | 27 +++++++++---------- .../pipeline/create/creator_plugins.py | 11 -------- .../ayon_core/pipeline/create/product_name.py | 13 ++------- .../ayon_core/pipeline/create/structures.py | 16 +++-------- 4 files changed, 17 insertions(+), 50 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6df437202e..0350c00977 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -29,7 +29,6 @@ from ayon_core.host import IWorkfileHost, IPublishHost from ayon_core.pipeline import Anatomy from ayon_core.pipeline.template_data import get_template_data from ayon_core.pipeline.plugin_discover import DiscoverResult -from ayon_core.pipeline.compatibility import is_product_base_type_supported from .exceptions import ( CreatorError, @@ -1237,6 +1236,15 @@ class CreateContext: """ creator = self._get_creator_in_create(creator_identifier) + if not hasattr(creator, "product_base_type"): + warn( + f"Provided creator {creator!r} doesn't have " + "product base type attribute defined. This will be " + "required in future.", + DeprecationWarning, + stacklevel=2 + ) + project_name = self.project_name if folder_entity is None: folder_path = self.get_current_folder_path() @@ -1290,23 +1298,12 @@ class CreateContext: "folderPath": folder_entity["path"], "task": task_entity["name"] if task_entity else None, "productType": creator.product_type, + # Add product base type if supported. Fallback to product type + "productBaseType": ( + creator.product_base_type or creator.product_type), "variant": variant } - # Add product base type if supported. - # TODO (antirotor): Once all creators support product base type - # remove this check. - if is_product_base_type_supported(): - - instance_data["productBaseType"] = creator.product_base_type - if creator.product_base_type is None: - msg = ( - f"Creator {creator_identifier} does not set " - "product base type. This will be required in future." - ) - warn(msg, DeprecationWarning, stacklevel=2) - self.log.warning(msg) - if active is not None: if not isinstance(active, bool): self.log.warning( diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 93dd763ed9..21d8596dea 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -6,7 +6,6 @@ import copy import os from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Any, Dict, Optional -from warnings import warn from ayon_core.lib import Logger, get_version_from_path from ayon_core.pipeline.plugin_discover import ( @@ -399,12 +398,6 @@ class BaseCreator(ABC): product_type = self.product_type if not product_base_type and not self.product_base_type: - warn( - f"Creator {self.identifier} does not support " - "product base type. This will be required in future.", - DeprecationWarning, - stacklevel=2, - ) product_base_type = product_type instance = CreatedInstance( @@ -547,10 +540,6 @@ class BaseCreator(ABC): project_entity (Optional[dict[str, Any]]): Project entity. """ - product_base_type = None - if hasattr(self, "product_base_type"): # noqa: E501 - product_base_type = self.product_base_type - if host_name is None: host_name = self.create_context.host_name diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f1076e51b3..c4ddb34652 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -57,16 +57,7 @@ def get_product_name_template( "task_types": task_type } - if not product_base_type: - warn( - "Product base type is not provided, please update your" - "creation code to include it. It will be required in " - "the future.", - DeprecationWarning, - stacklevel=2 - ) filtering_criteria["product_base_types"] = product_base_type - matching_profile = filter_profiles(profiles, filtering_criteria) template = None @@ -127,8 +118,8 @@ def get_product_name( Args: project_name (str): Project name. - task_name (str): Task name. - task_type (str): Task type. + task_name (Optional[str]): Task name. + task_type (Optional[str]): Task type. host_name (str): Host name. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index e93270b357..dfa9d69938 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -4,7 +4,6 @@ from uuid import uuid4 from enum import Enum import typing from typing import Optional, Dict, List, Any -from warnings import warn from ayon_core.lib.attribute_definitions import ( AbstractAttrDef, @@ -521,17 +520,9 @@ class CreatedInstance: product_base_type: Optional[str] = None ): """Initialize CreatedInstance.""" - if is_product_base_type_supported(): - if not hasattr(creator, "product_base_type"): - warn( - f"Provided creator {creator!r} doesn't have " - "product base type attribute defined. This will be " - "required in future.", - DeprecationWarning, - stacklevel=2 - ) - elif not product_base_type: - product_base_type = creator.product_base_type + # fallback to product type for backward compatibility + if not product_base_type: + product_base_type = creator.product_base_type or product_type self._creator = creator creator_identifier = creator.identifier @@ -587,7 +578,6 @@ class CreatedInstance: self._data["productName"] = product_name if is_product_base_type_supported(): - data.pop("productBaseType", None) self._data["productBaseType"] = product_base_type self._data["active"] = data.get("active", True) From 1cddb86918fbd2806cba78c549c2a0ca971caa30 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:17:19 +0100 Subject: [PATCH 42/90] =?UTF-8?q?=E2=9A=97=EF=B8=8F=20fix=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pipeline/create/test_product_name.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py index a8a8566aa8..03b13d2c25 100644 --- a/tests/client/ayon_core/pipeline/create/test_product_name.py +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -20,7 +20,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_matching_profile_with_replacements( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -33,7 +32,6 @@ class TestGetProductNameTemplate: "template": ("{task}-{Task}-{TASK}-{family}-{Family}" "-{FAMILY}-{asset}-{Asset}-{ASSET}") } - mock_is_supported.return_value = False result = get_product_name_template( project_name="proj", @@ -54,7 +52,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_no_matching_profile_uses_default( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -62,7 +59,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = False assert ( get_product_name_template( @@ -81,7 +77,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_custom_default_template_used( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -89,7 +84,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = False custom_default = "{variant}_{family}" assert ( @@ -111,7 +105,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_product_base_type_warns_when_supported_and_missing( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, mock_warn, @@ -120,7 +113,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = True get_product_name_template( project_name="proj", @@ -137,7 +129,6 @@ class TestGetProductNameTemplate: "is_product_base_type_supported") def test_product_base_type_added_to_filtering_when_provided( self, - mock_is_supported, mock_filter_profiles, mock_get_settings, ): @@ -145,7 +136,6 @@ class TestGetProductNameTemplate: "core": {"tools": {"creator": {"product_name_profiles": []}}} } mock_filter_profiles.return_value = None - mock_is_supported.return_value = True get_product_name_template( project_name="proj", @@ -308,8 +298,6 @@ class TestGetProductName: ) @patch("ayon_core.pipeline.create.product_name.warn") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") @patch("ayon_core.pipeline.create.product_name.get_product_name_template") @patch("ayon_core.pipeline.create.product_name." "StringTemplate.format_strict_template") @@ -319,11 +307,10 @@ class TestGetProductName: mock_prepare, mock_format, mock_get_tmpl, - mock_is_supported, mock_warn, ): mock_get_tmpl.return_value = "{product[basetype]}_{variant}" - mock_is_supported.return_value = True + mock_prepare.return_value = { "product": {"type": "model"}, "variant": "Main", From 794bb716b268a385dd978dab1099bba13ffb01de Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:25:54 +0100 Subject: [PATCH 43/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20small=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/product_name.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index c4ddb34652..bf3d3b0abc 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -54,11 +54,9 @@ def get_product_name_template( "product_types": product_type, "hosts": host_name, "tasks": task_name, - "task_types": task_type + "task_types": task_type, + "product_base_types": product_base_type, } - - filtering_criteria["product_base_types"] = product_base_type - matching_profile = filter_profiles(profiles, filtering_criteria) template = None if matching_profile: From 00e2e3c2ade7b46c81377f083f8a321aae90c3e0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 15:33:43 +0100 Subject: [PATCH 44/90] =?UTF-8?q?=F0=9F=8E=9B=EF=B8=8F=20fix=20type=20hint?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/product_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index bf3d3b0abc..f5a7418b57 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -83,8 +83,8 @@ def get_product_name_template( def get_product_name( project_name: str, task_name: str, - task_type: str, - host_name: str, + task_type: Optional[str], + host_name: Optional[str], product_type: str, variant: str, default_template: Optional[str] = None, From e6007b2cee79e82521850ce5d3658eb5a8d7279d Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:25:10 +0100 Subject: [PATCH 45/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20fixes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit type hints, checks --- client/ayon_core/pipeline/create/context.py | 18 +++++++++--------- .../pipeline/create/creator_plugins.py | 4 ++-- .../ayon_core/pipeline/create/product_name.py | 16 ++++++---------- client/ayon_core/pipeline/create/structures.py | 4 +--- 4 files changed, 18 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 0350c00977..a09f1924da 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -766,6 +766,15 @@ class CreateContext: "and skipping: %s", creator_identifier, creator_class ) continue + if not creator_class.product_base_type: + warn( + f"Provided creator {creator_class!r} doesn't have " + "product base type attribute defined. This will be " + "required in future.", + DeprecationWarning, + stacklevel=2 + ) + continue # Filter by host name if ( @@ -1236,15 +1245,6 @@ class CreateContext: """ creator = self._get_creator_in_create(creator_identifier) - if not hasattr(creator, "product_base_type"): - warn( - f"Provided creator {creator!r} doesn't have " - "product base type attribute defined. This will be " - "required in future.", - DeprecationWarning, - stacklevel=2 - ) - project_name = self.project_name if folder_entity is None: folder_path = self.get_current_folder_path() diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 21d8596dea..92eb3b6946 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -405,7 +405,7 @@ class BaseCreator(ABC): product_name=product_name, data=data, creator=self, - product_base_type=product_base_type + product_base_type=product_base_type, ) self._add_instance_to_context(instance) return instance @@ -516,7 +516,7 @@ class BaseCreator(ABC): self, project_name: str, folder_entity: dict[str, Any], - task_entity: dict[str, Any], + task_entity: Optional[dict[str, Any]], variant: str, host_name: Optional[str] = None, instance: Optional[CreatedInstance] = None, diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f5a7418b57..cc1014173c 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -82,9 +82,9 @@ def get_product_name_template( def get_product_name( project_name: str, - task_name: str, + task_name: Optional[str], task_type: Optional[str], - host_name: Optional[str], + host_name: str, product_type: str, variant: str, default_template: Optional[str] = None, @@ -180,14 +180,7 @@ def get_product_name( task_short = task_types_by_name.get(task_type, {}).get("shortName") task_value["short"] = task_short - # look what we have to do to make mypy happy. We should stop using - # those undefined dict based types. - product: dict[str, str] = { - "type": product_type, - "baseType": product_base_type - } if not product_base_type and "{product[basetype]}" in template.lower(): - product["baseType"] = product_type warn( "You have Product base type in product name template, " "but it is not provided by the creator, please update your " @@ -200,7 +193,10 @@ def get_product_name( "variant": variant, "family": product_type, "task": task_value, - "product": product, + "product": { + "type": product_type, + "baseType": product_base_type or product_type, + } } if dynamic_data: diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index dfa9d69938..6f53a61b25 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -12,7 +12,6 @@ from ayon_core.lib.attribute_definitions import ( deserialize_attr_defs, ) -from ayon_core.pipeline.compatibility import is_product_base_type_supported from ayon_core.pipeline import ( AYON_INSTANCE_ID, @@ -577,8 +576,7 @@ class CreatedInstance: self._data["productType"] = product_type self._data["productName"] = product_name - if is_product_base_type_supported(): - self._data["productBaseType"] = product_base_type + self._data["productBaseType"] = product_base_type self._data["active"] = data.get("active", True) self._data["creator_identifier"] = creator_identifier From 700b025024faf91a54a904d10b0bd7b45fa81f6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:34:01 +0100 Subject: [PATCH 46/90] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20move=20plugin=20chec?= =?UTF-8?q?k=20earlier,=20fix=20hints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/pipeline/create/context.py | 16 +++++++++------- client/ayon_core/pipeline/create/product_name.py | 8 ++++---- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index a09f1924da..f1a5b0e9f8 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -759,13 +759,6 @@ class CreateContext: ) for creator_class in report.plugins: - creator_identifier = creator_class.identifier - if creator_identifier in creators: - self.log.warning( - "Duplicate Creator identifier: '%s'. Using first Creator " - "and skipping: %s", creator_identifier, creator_class - ) - continue if not creator_class.product_base_type: warn( f"Provided creator {creator_class!r} doesn't have " @@ -776,6 +769,15 @@ class CreateContext: ) continue + creator_identifier = creator_class.identifier + if creator_identifier in creators: + self.log.warning( + "Duplicate Creator identifier: '%s'. Using first Creator " + "and skipping: %s", creator_identifier, creator_class + ) + continue + + # Filter by host name if ( creator_class.host_name diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index cc1014173c..7f87145595 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -19,8 +19,8 @@ from .exceptions import TaskNotSetError, TemplateFillError def get_product_name_template( project_name: str, product_type: str, - task_name: str, - task_type: str, + task_name: Optional[str], + task_type: Optional[str], host_name: str, default_template: Optional[str] = None, project_settings: Optional[dict[str, Any]] = None, @@ -33,8 +33,8 @@ def get_product_name_template( 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. + task_name (Optional[str]): Name of task in which context the product is created. + task_type (Optional[str]): Type of task in which context the product is created. 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. From bb430342d8b7b7d395d8af99916297f52353d047 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:35:49 +0100 Subject: [PATCH 47/90] :dog: fix linter --- client/ayon_core/pipeline/create/context.py | 1 - client/ayon_core/pipeline/create/product_name.py | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index f1a5b0e9f8..2b9556d005 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -777,7 +777,6 @@ class CreateContext: ) continue - # Filter by host name if ( creator_class.host_name diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 7f87145595..d59e8f9b67 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -33,8 +33,10 @@ def get_product_name_template( 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 (Optional[str]): Name of task in which context the product is created. - task_type (Optional[str]): Type of task in which context the product is created. + task_name (Optional[str]): Name of task in which context the + product is created. + task_type (Optional[str]): Type of task in which context the + product is created. 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. From b0005180f269f6139ca355c897052e4b417116eb Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:40:27 +0100 Subject: [PATCH 48/90] :alembic: fix tests --- .../pipeline/create/test_product_name.py | 33 ------------------- 1 file changed, 33 deletions(-) diff --git a/tests/client/ayon_core/pipeline/create/test_product_name.py b/tests/client/ayon_core/pipeline/create/test_product_name.py index 03b13d2c25..7181e18b43 100644 --- a/tests/client/ayon_core/pipeline/create/test_product_name.py +++ b/tests/client/ayon_core/pipeline/create/test_product_name.py @@ -16,8 +16,6 @@ from ayon_core.pipeline.create.exceptions import ( class TestGetProductNameTemplate: @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_matching_profile_with_replacements( self, mock_filter_profiles, @@ -48,8 +46,6 @@ class TestGetProductNameTemplate: @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_no_matching_profile_uses_default( self, mock_filter_profiles, @@ -73,8 +69,6 @@ class TestGetProductNameTemplate: @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_custom_default_template_used( self, mock_filter_profiles, @@ -98,35 +92,8 @@ class TestGetProductNameTemplate: == custom_default ) - @patch("ayon_core.pipeline.create.product_name.warn") @patch("ayon_core.pipeline.create.product_name.get_project_settings") @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") - def test_product_base_type_warns_when_supported_and_missing( - self, - mock_filter_profiles, - mock_get_settings, - mock_warn, - ): - mock_get_settings.return_value = { - "core": {"tools": {"creator": {"product_name_profiles": []}}} - } - mock_filter_profiles.return_value = None - - get_product_name_template( - project_name="proj", - product_type="model", - task_name="modeling", - task_type="Modeling", - host_name="maya", - ) - mock_warn.assert_called_once() - - @patch("ayon_core.pipeline.create.product_name.get_project_settings") - @patch("ayon_core.pipeline.create.product_name.filter_profiles") - @patch("ayon_core.pipeline.create.product_name." - "is_product_base_type_supported") def test_product_base_type_added_to_filtering_when_provided( self, mock_filter_profiles, From 3a24db94f51915440b0a963b1ef9402d1681f0c0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:45:17 +0100 Subject: [PATCH 49/90] :memo: log deprecation warning --- client/ayon_core/pipeline/create/context.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 2b9556d005..6495a9d6e9 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -760,13 +760,15 @@ class CreateContext: for creator_class in report.plugins: if not creator_class.product_base_type: - warn( - f"Provided creator {creator_class!r} doesn't have " + message = (f"Provided creator {creator_class!r} doesn't have " "product base type attribute defined. This will be " - "required in future.", + "required in future.") + warn( + message, DeprecationWarning, stacklevel=2 ) + self.log.warning(message) continue creator_identifier = creator_class.identifier From 64f549c4956376b2c8fc92fc80cecfeb136a6432 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:48:52 +0100 Subject: [PATCH 50/90] ugly thing in name of compatibility? --- client/ayon_core/pipeline/create/product_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index d59e8f9b67..f10e375fb1 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -170,7 +170,7 @@ def get_product_name( "type": task_type, } if "{task}" in template.lower(): - task_value["name"] = task_name + task_value = task_name elif "{task[short]}" in template.lower(): if project_entity is None: From 1f88b0031dcdd72507c966cfb1dec0352fdc5ed3 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 26 Nov 2025 17:56:35 +0100 Subject: [PATCH 51/90] :recycle: fix discovery --- client/ayon_core/pipeline/create/context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 6495a9d6e9..08b6574db8 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -769,7 +769,6 @@ class CreateContext: stacklevel=2 ) self.log.warning(message) - continue creator_identifier = creator_class.identifier if creator_identifier in creators: From ba6a9bdca4a8c301c8bf8122f4a0bd9afa620a46 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 00:41:34 +0100 Subject: [PATCH 52/90] Allow OCIO color management profiles that have no matching profile or do match a profile with "disabled" status to be considered as NOT color managed. So that you can specify a particular part of the project to NOT be OCIO color managed. --- client/ayon_core/pipeline/colorspace.py | 56 ++++++++++++++++--------- server/settings/main.py | 1 + 2 files changed, 37 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 41241e17ca..db7d287cf1 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -7,6 +7,7 @@ import platform import tempfile import warnings from copy import deepcopy +from dataclasses import dataclass import ayon_api @@ -25,6 +26,17 @@ from ayon_core.pipeline.load import get_representation_path_with_anatomy log = Logger.get_logger(__name__) +@dataclass +class ConfigData: + """OCIO Config to use in a certain context. + + When enabled and no path/template are set, it will be considered invalid + and will error on OCIO path not found. Enabled must be False to explicitly + allow OCIO to be disabled.""" + path: str = "" + template: str = "" + enabled: bool = True + class CachedData: remapping = {} @@ -710,7 +722,7 @@ def _get_config_path_from_profile_data( template_data (dict[str, Any]): Template data. Returns: - dict[str, str]: Config data with path and template. + ConfigData: Config data with path and template. """ template = profile[profile_type] result = StringTemplate.format_strict_template( @@ -719,12 +731,12 @@ def _get_config_path_from_profile_data( normalized_path = str(result.normalized()) if not os.path.exists(normalized_path): log.warning(f"Path was not found '{normalized_path}'.") - return None + return ConfigData() # Return invalid config data - return { - "path": normalized_path, - "template": template - } + return ConfigData( + path=normalized_path, + template=template + ) def _get_global_config_data( @@ -735,7 +747,7 @@ def _get_global_config_data( imageio_global, folder_id, log, -): +) -> ConfigData: """Get global config data. Global config from core settings is using profiles that are based on @@ -759,8 +771,7 @@ def _get_global_config_data( log (logging.Logger): Logger object. Returns: - Union[dict[str, str], None]: Config data with path and template - or None. + ConfigData: Config data with path and template. """ task_name = task_type = None @@ -779,12 +790,14 @@ def _get_global_config_data( ) if profile is None: log.info(f"No config profile matched filters {str(filter_values)}") - return None + return ConfigData(enabled=False) profile_type = profile["type"] - if profile_type in ("builtin_path", "custom_path"): + if profile_type in {"builtin_path", "custom_path"}: return _get_config_path_from_profile_data( profile, profile_type, template_data) + elif profile_type == "disabled": + return ConfigData(enabled=False) # TODO decide if this is the right name for representation repre_name = "ocioconfig" @@ -798,7 +811,7 @@ def _get_global_config_data( "Colorspace OCIO config path cannot be set. " "Profile is set to published product but `Product name` is empty." ) - return None + return ConfigData() folder_info = template_data.get("folder") if not folder_info: @@ -819,7 +832,7 @@ def _get_global_config_data( ) if not folder_entity: log.warning(f"Folder entity '{folder_path}' was not found..") - return None + return ConfigData() folder_id = folder_entity["id"] product_entities_by_name = { @@ -855,7 +868,7 @@ def _get_global_config_data( log.info( f"Product '{product_name}' does not have available any versions." ) - return None + return ConfigData() # Find 'ocioconfig' representation entity repre_entity = ayon_api.get_representation_by_name( @@ -868,15 +881,15 @@ def _get_global_config_data( f"Representation '{repre_name}'" f" not found on product '{product_name}'." ) - return None + return ConfigData() path = get_representation_path_with_anatomy(repre_entity, anatomy) template = repre_entity["attrib"]["template"] - return { - "path": path, - "template": template, - } + return ConfigData( + path=path, + template=template + ) def get_imageio_config_preset( @@ -1015,7 +1028,10 @@ def get_imageio_config_preset( host_ocio_config["filepath"], template_data ) - if not config_data: + if not config_data.enabled: + return {} # OCIO management disabled + + if not config_data.path: raise FileExistsError( "No OCIO config found in settings. It is" " either missing or there is typo in path inputs" diff --git a/server/settings/main.py b/server/settings/main.py index cca885303f..3bd9549116 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -59,6 +59,7 @@ def _ocio_config_profile_types(): {"value": "builtin_path", "label": "AYON built-in OCIO config"}, {"value": "custom_path", "label": "Path to OCIO config"}, {"value": "published_product", "label": "Published product"}, + {"value": "disabled", "label": "Disable OCIO management"}, ] From ab8a93b4a4455ba1ecf639f00d325051bb8bf8de Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 00:57:46 +0100 Subject: [PATCH 53/90] Cosmetics --- client/ayon_core/pipeline/colorspace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index db7d287cf1..a7d205d48e 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -26,6 +26,7 @@ from ayon_core.pipeline.load import get_representation_path_with_anatomy log = Logger.get_logger(__name__) + @dataclass class ConfigData: """OCIO Config to use in a certain context. From a73d8f947d25f6243f293204a1405f1e47065fef Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 01:01:51 +0100 Subject: [PATCH 54/90] Fix return value --- client/ayon_core/pipeline/colorspace.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index a7d205d48e..7a4d9dda50 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -1038,7 +1038,10 @@ def get_imageio_config_preset( " either missing or there is typo in path inputs" ) - return config_data + return { + "path": config_data.path, + "template": config_data.template, + } def _get_host_config_data(templates, template_data): From fd1b3b0e64ea1516139608659ee9b013f82d372f Mon Sep 17 00:00:00 2001 From: TobiasPharos Date: Thu, 2 Oct 2025 11:22:25 +0200 Subject: [PATCH 55/90] fix for "replace_with_published_scene_path" Published scene file has to be of productType/family "workfile" --- client/ayon_core/pipeline/publish/lib.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 1f983808b0..3756746ee9 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -812,7 +812,20 @@ def replace_with_published_scene_path(instance, replace_in_path=True): template_data["comment"] = None anatomy = instance.context.data["anatomy"] - template = anatomy.get_template_item("publish", "default", "path") + project_name = anatomy.project_name + task_entity = instance.data.get("taskEntity", {}) + project_settings = get_project_settings(project_name) + template_name = get_publish_template_name( + project_name=project_name, + host_name=instance.context.data.get("hostName", os.environ["AYON_HOST_NAME"]), + # publish template has to match productType "workfile", + # otherwise default template will be used: + product_type="workfile", + task_name=task_entity.get("name", os.environ["AYON_TASK_NAME"]), + task_type=task_entity.get("taskType"), + project_settings=project_settings, + ) + template = anatomy.get_template_item("publish", template_name, "path") template_filled = template.format_strict(template_data) file_path = os.path.normpath(template_filled) From 81fb1e73c4f5c4776ad133e7ad488afea54ea973 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 17 Oct 2025 14:14:52 +0200 Subject: [PATCH 56/90] handle project settings and task entity --- client/ayon_core/pipeline/publish/lib.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 3756746ee9..555f4b1894 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -813,16 +813,20 @@ def replace_with_published_scene_path(instance, replace_in_path=True): anatomy = instance.context.data["anatomy"] project_name = anatomy.project_name - task_entity = instance.data.get("taskEntity", {}) - project_settings = get_project_settings(project_name) + task_name = task_type = None + task_entity = instance.data.get("taskEntity") + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + project_settings = instance.context.data["project_settings"] template_name = get_publish_template_name( project_name=project_name, - host_name=instance.context.data.get("hostName", os.environ["AYON_HOST_NAME"]), + host_name=instance.context.data["hostName"], # publish template has to match productType "workfile", # otherwise default template will be used: product_type="workfile", - task_name=task_entity.get("name", os.environ["AYON_TASK_NAME"]), - task_type=task_entity.get("taskType"), + task_name=task_name, + task_type=task_type, project_settings=project_settings, ) template = anatomy.get_template_item("publish", template_name, "path") From 67364633f0d3f68278d1d292a83eebd5e6ffd925 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 10:56:02 +0100 Subject: [PATCH 57/90] :robot: implement copilot suggestions --- client/ayon_core/pipeline/create/context.py | 41 +++++++++---------- .../pipeline/create/creator_plugins.py | 5 ++- .../ayon_core/pipeline/create/product_name.py | 2 +- 3 files changed, 24 insertions(+), 24 deletions(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 08b6574db8..c379dd38f2 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -758,18 +758,6 @@ class CreateContext: "Skipping abstract Creator '%s'", str(creator_class) ) - for creator_class in report.plugins: - if not creator_class.product_base_type: - message = (f"Provided creator {creator_class!r} doesn't have " - "product base type attribute defined. This will be " - "required in future.") - warn( - message, - DeprecationWarning, - stacklevel=2 - ) - self.log.warning(message) - creator_identifier = creator_class.identifier if creator_identifier in creators: self.log.warning( @@ -783,19 +771,17 @@ class CreateContext: creator_class.host_name and creator_class.host_name != self.host_name ): - self.log.info(( - "Creator's host name \"{}\"" - " is not supported for current host \"{}\"" - ).format(creator_class.host_name, self.host_name)) + self.log.info( + ( + 'Creator\'s host name "{}"' + ' is not supported for current host "{}"' + ).format(creator_class.host_name, self.host_name) + ) continue # TODO report initialization error try: - creator = creator_class( - project_settings, - self, - self.headless - ) + creator = creator_class(project_settings, self, self.headless) except Exception: self.log.error( f"Failed to initialize plugin: {creator_class}", @@ -803,6 +789,19 @@ class CreateContext: ) continue + if not creator.product_base_type: + message = ( + f"Provided creator {creator!r} doesn't have " + "product base type attribute defined. This will be " + "required in future." + ) + warn( + message, + DeprecationWarning, + stacklevel=2 + ) + self.log.warning(message) + if not creator.enabled: disabled_creators[creator_identifier] = creator continue diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 92eb3b6946..8d1dbd0f2e 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -290,7 +290,8 @@ class BaseCreator(ABC): def identifier(self): """Identifier of creator (must be unique). - Default implementation returns plugin's product type. + Default implementation returns plugin's product base type, or falls back + to product type if product base type is not set. """ identifier = self.product_base_type @@ -388,7 +389,7 @@ class BaseCreator(ABC): product_type (Optional[str]): Product type, object attribute 'product_type' is used if not passed. product_base_type (Optional[str]): Product base type, object - attribute 'product_type' is used if not passed. + attribute 'product_base_type' is used if not passed. Returns: CreatedInstance: Created instance. diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index f10e375fb1..2bf84db0f4 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -197,7 +197,7 @@ def get_product_name( "task": task_value, "product": { "type": product_type, - "baseType": product_base_type or product_type, + "basetype": product_base_type or product_type, } } From c33795b68a6d6bd374428c07e623a23e99861a45 Mon Sep 17 00:00:00 2001 From: TobiasPharos Date: Tue, 25 Nov 2025 10:07:18 +0100 Subject: [PATCH 58/90] get product_type from workfile instance --- client/ayon_core/pipeline/publish/lib.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 555f4b1894..2187ef0304 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -822,9 +822,7 @@ def replace_with_published_scene_path(instance, replace_in_path=True): template_name = get_publish_template_name( project_name=project_name, host_name=instance.context.data["hostName"], - # publish template has to match productType "workfile", - # otherwise default template will be used: - product_type="workfile", + product_type=workfile_instance.data["productType"], task_name=task_name, task_type=task_type, project_settings=project_settings, From f8e8ab2b27f0619b98bca16069da99d033ea0ee0 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 10:58:13 +0100 Subject: [PATCH 59/90] :dog: fix long line --- client/ayon_core/pipeline/create/creator_plugins.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index 8d1dbd0f2e..a034451d83 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -290,8 +290,8 @@ class BaseCreator(ABC): def identifier(self): """Identifier of creator (must be unique). - Default implementation returns plugin's product base type, or falls back - to product type if product base type is not set. + Default implementation returns plugin's product base type, + or falls back to product type if product base type is not set. """ identifier = self.product_base_type From bb8f214e475ec657730f8bbb861e99896795005a Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 11:59:20 +0100 Subject: [PATCH 60/90] :bug: fix the abstract plugin debug print --- client/ayon_core/pipeline/create/context.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index c379dd38f2..d8cb9d1b9e 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -755,9 +755,11 @@ class CreateContext: self.creator_discover_result = report for creator_class in report.abstract_plugins: self.log.debug( - "Skipping abstract Creator '%s'", str(creator_class) + "Skipping abstract Creator '%s'", + str(creator_class) ) + for creator_class in report.plugins: creator_identifier = creator_class.identifier if creator_identifier in creators: self.log.warning( From 2a5210ccc535e13ae4aec24415a108505982ba2b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:10:55 +0100 Subject: [PATCH 61/90] Update client/ayon_core/plugins/publish/extract_oiio_postprocess.py Co-authored-by: Mustafa Zaky Jafar --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 33384e0185..1130a86fb6 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -62,7 +62,8 @@ class ExtractOIIOPostProcess(publish.Extractor): # it's included in the profile check. As such, an instance may have # a different profile applied per representation. profile = self._get_profile( - instance + instance, + repre ) if not profile: continue From 877a9fdecd61d7935e0bedf62344d2cc14c785d8 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:38:35 +0100 Subject: [PATCH 62/90] Refactor profile `hosts` -> `host_names` --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 2 +- server/settings/publish_plugins.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 1130a86fb6..3228e418b5 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -272,7 +272,7 @@ class ExtractOIIOPostProcess(publish.Extractor): repre_name: str = repre["name"] repre_ext: str = repre["ext"] filtering_criteria = { - "hosts": host_name, + "host_names": host_name, "product_types": product_type, "product_names": product_name, "task_names": task_name, diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index aa86398582..9b490ab208 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -638,7 +638,7 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): default_factory=list, title="Product types" ) - hosts: list[str] = SettingsField( + host_names: list[str] = SettingsField( default_factory=list, title="Host names" ) From e2727ad15e7db7c5789ff2a67ffabbbec32f917a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:41:31 +0100 Subject: [PATCH 63/90] Cosmetics + type hints --- .../plugins/publish/extract_oiio_postprocess.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 3228e418b5..c7ae4c3910 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -261,7 +261,11 @@ class ExtractOIIOPostProcess(publish.Extractor): output_extension) return os.path.join(output_dir, new_file_name) - def _get_profile(self, instance: pyblish.api.Instance, repre: dict) -> Optional[dict[str, Any]]: + def _get_profile( + self, + instance: pyblish.api.Instance, + repre: dict + ) -> Optional[dict[str, Any]]: """Returns profile if it should process this instance.""" host_name = instance.context.data["hostName"] product_type = instance.data["productType"] @@ -296,7 +300,7 @@ class ExtractOIIOPostProcess(publish.Extractor): return profile - def _repre_is_valid(self, repre) -> bool: + def _repre_is_valid(self, repre: dict) -> bool: """Validation if representation should be processed. Args: @@ -325,7 +329,12 @@ class ExtractOIIOPostProcess(publish.Extractor): return True - def _mark_original_repre_for_deletion(self, repre, profile, added_review): + def _mark_original_repre_for_deletion( + self, + repre: dict, + profile: dict, + added_review: bool + ): """If new transcoded representation created, delete old.""" if not repre.get("tags"): repre["tags"] = [] From c1210b297788cd35190edc300fe8ebccfc5bb2a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 14:42:34 +0100 Subject: [PATCH 64/90] Also skip early if no representations in the data. --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index c7ae4c3910..7ea6ba50e3 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -44,7 +44,7 @@ class ExtractOIIOPostProcess(publish.Extractor): self.log.debug("No profiles present for OIIO Post Process") return - if "representations" not in instance.data: + if not instance.data.get("representations"): self.log.debug("No representations, skipping.") return From 596612cc99e6e0238d77ca4278f4e926fc41380f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 15:02:11 +0100 Subject: [PATCH 65/90] Move over product types in settings so order makes more sense --- server/settings/publish_plugins.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 9b490ab208..a5b84354bd 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -633,12 +633,8 @@ class ExtractOIIOPostProcessOutputModel(BaseSettingsModel): class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): - product_types: list[str] = SettingsField( - section="Profile", - default_factory=list, - title="Product types" - ) host_names: list[str] = SettingsField( + section="Profile", default_factory=list, title="Host names" ) @@ -651,6 +647,10 @@ class ExtractOIIOPostProcessProfileModel(BaseSettingsModel): default_factory=list, title="Task names" ) + product_types: list[str] = SettingsField( + default_factory=list, + title="Product types" + ) product_names: list[str] = SettingsField( default_factory=list, title="Product names" From a7e02c19e5c7443638283a82b3f7ecab613edf55 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 15:30:53 +0100 Subject: [PATCH 66/90] Update client/ayon_core/plugins/publish/extract_oiio_postprocess.py Co-authored-by: Mustafa Zaky Jafar --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 7ea6ba50e3..7f3ba38963 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -40,6 +40,9 @@ class ExtractOIIOPostProcess(publish.Extractor): options = None def process(self, instance): + if instance.data.get("farm"): + self.log.debug("Should be processed on farm, skipping.") + return if not self.profiles: self.log.debug("No profiles present for OIIO Post Process") return From 0b942a062f3255a43a3d2a1a1f830f473396860b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 27 Nov 2025 15:31:39 +0100 Subject: [PATCH 67/90] Cosmetics --- client/ayon_core/plugins/publish/extract_oiio_postprocess.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py index 7f3ba38963..2b432f2a0a 100644 --- a/client/ayon_core/plugins/publish/extract_oiio_postprocess.py +++ b/client/ayon_core/plugins/publish/extract_oiio_postprocess.py @@ -43,6 +43,7 @@ class ExtractOIIOPostProcess(publish.Extractor): if instance.data.get("farm"): self.log.debug("Should be processed on farm, skipping.") return + if not self.profiles: self.log.debug("No profiles present for OIIO Post Process") return From feb16122009857c4192914b1b3b9d9ca4936d4f7 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 27 Nov 2025 17:18:35 +0100 Subject: [PATCH 68/90] =?UTF-8?q?=E2=9E=96=20remove=20argument?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/ayon_core/tools/publisher/models/create.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 86f0cd2d07..b8518a7de6 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -665,7 +665,6 @@ class CreateModel: kwargs = { "instance": instance, "project_entity": project_entity, - "product_base_type": creator.product_base_type, } # Backwards compatibility for 'project_entity' argument # - 'get_product_name' signature changed 24/07/08 From 8a0e1afcb37ef0843e1ae9bf714dfa949eb3bab0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 28 Nov 2025 23:16:02 +0100 Subject: [PATCH 69/90] Remove unused function: `split_cmd_args` --- client/ayon_core/lib/transcoding.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 076ee79665..726ea62542 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -1281,24 +1281,6 @@ def oiio_color_convert( run_subprocess(oiio_cmd, logger=logger) -def split_cmd_args(in_args): - """Makes sure all entered arguments are separated in individual items. - - Split each argument string with " -" to identify if string contains - one or more arguments. - Args: - in_args (list): of arguments ['-n', '-d uint10'] - Returns - (list): ['-n', '-d', 'unint10'] - """ - splitted_args = [] - for arg in in_args: - if not arg.strip(): - continue - splitted_args.extend(arg.split(" ")) - return splitted_args - - def get_rescaled_command_arguments( application, input_path, From 72249691801ac1784e1bf85a2b070c62a03ccfa5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Sat, 29 Nov 2025 16:13:55 +0100 Subject: [PATCH 70/90] fix product name template filtering --- client/ayon_core/pipeline/create/product_name.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index ecffa4a340..5596cec0ce 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -41,8 +41,8 @@ def get_product_name_template( profiles = tools_settings["creator"]["product_name_profiles"] filtering_criteria = { "product_types": product_type, - "hosts": host_name, - "tasks": task_name, + "host_names": host_name, + "task_names": task_name, "task_types": task_type } From 930454ad08832328e98c0b8d2473eafbb3623c75 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sun, 30 Nov 2025 21:38:36 +0100 Subject: [PATCH 71/90] Fix missing settings conversion --- server/settings/conversion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/conversion.py b/server/settings/conversion.py index 846b91edab..757818a9ff 100644 --- a/server/settings/conversion.py +++ b/server/settings/conversion.py @@ -164,5 +164,6 @@ def convert_settings_overrides( ) -> dict[str, Any]: _convert_imageio_configs_0_3_1(overrides) _convert_imageio_configs_0_4_5(overrides) + _convert_imageio_configs_1_6_5(overrides) _convert_publish_plugins(overrides) return overrides From b0d153ce8745d304108468d78c5ab630f28b0255 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:30:22 +0100 Subject: [PATCH 72/90] remove python from pyproject toml --- client/pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/client/pyproject.toml b/client/pyproject.toml index c98591b707..5ae71de18b 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -3,7 +3,6 @@ name="core" description="AYON core addon." [tool.poetry.dependencies] -python = ">=3.9.1,<3.10" markdown = "^3.4.1" clique = "1.6.*" jsonschema = "^2.6.0" From 79aa108da794214285c98eea05a6c8e43b6b71aa Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 1 Dec 2025 12:02:25 +0000 Subject: [PATCH 73/90] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a3e1a6c939..168eaa7c21 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.11+dev" +__version__ = "1.6.12" diff --git a/package.py b/package.py index 62231060f0..0b1bb92a3a 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.11+dev" +version = "1.6.12" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index d568edefc0..7b2e3f9e7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.11+dev" +version = "1.6.12" description = "" authors = ["Ynput Team "] readme = "README.md" From 0e34fb6474d0f30f3b164ac0579c61edf7bcb41c Mon Sep 17 00:00:00 2001 From: Ynbot Date: Mon, 1 Dec 2025 12:03:02 +0000 Subject: [PATCH 74/90] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 168eaa7c21..3e6b3794b1 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.12" +__version__ = "1.6.12+dev" diff --git a/package.py b/package.py index 0b1bb92a3a..fbf7021b8e 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.12" +version = "1.6.12+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 7b2e3f9e7b..208bbec85f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.12" +version = "1.6.12+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 31b65f22ae9860dbe02209e5914adbdc3c698097 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 1 Dec 2025 12:03:58 +0000 Subject: [PATCH 75/90] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 7fc253b1b8..98e4b46e07 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.12 - 1.6.11 - 1.6.10 - 1.6.9 From 215d077f3165956b422fd9477ccd364313f60be5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Dec 2025 13:15:19 +0100 Subject: [PATCH 76/90] pass host_name to 'get_loader_action_plugin_paths' --- client/ayon_core/pipeline/actions/loader.py | 22 +++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/actions/loader.py b/client/ayon_core/pipeline/actions/loader.py index 92de9c6cf8..53cc52d39f 100644 --- a/client/ayon_core/pipeline/actions/loader.py +++ b/client/ayon_core/pipeline/actions/loader.py @@ -70,7 +70,7 @@ from dataclasses import dataclass import ayon_api from ayon_core import AYON_CORE_ROOT -from ayon_core.lib import StrEnum, Logger +from ayon_core.lib import StrEnum, Logger, is_func_signature_supported from ayon_core.host import AbstractHost from ayon_core.addon import AddonsManager, IPluginPaths from ayon_core.settings import get_studio_settings, get_project_settings @@ -752,6 +752,7 @@ class LoaderActionsContext: def _get_plugins(self) -> dict[str, LoaderActionPlugin]: if self._plugins is None: + host_name = self.get_host_name() addons_manager = self.get_addons_manager() all_paths = [ os.path.join(AYON_CORE_ROOT, "plugins", "loader") @@ -759,7 +760,24 @@ class LoaderActionsContext: for addon in addons_manager.addons: if not isinstance(addon, IPluginPaths): continue - paths = addon.get_loader_action_plugin_paths() + + try: + if is_func_signature_supported( + addon.get_loader_action_plugin_paths, + host_name + ): + paths = addon.get_loader_action_plugin_paths( + host_name + ) + else: + paths = addon.get_loader_action_plugin_paths() + except Exception: + self._log.warning( + "Failed to get plugin paths for addon", + exc_info=True + ) + continue + if paths: all_paths.extend(paths) From cd499f495136599f3aeb61618a0dfe2c49df54dc Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:18:16 +0100 Subject: [PATCH 77/90] change IPluginPaths interface method signature --- client/ayon_core/addon/interfaces.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/addon/interfaces.py b/client/ayon_core/addon/interfaces.py index bc44fd2d2e..0a17ec9fb9 100644 --- a/client/ayon_core/addon/interfaces.py +++ b/client/ayon_core/addon/interfaces.py @@ -185,9 +185,14 @@ class IPluginPaths(AYONInterface): """ return self._get_plugin_paths_by_type("inventory") - def get_loader_action_plugin_paths(self) -> list[str]: + def get_loader_action_plugin_paths( + self, host_name: Optional[str] + ) -> list[str]: """Receive loader action plugin paths. + Args: + host_name (Optional[str]): Current host name. + Returns: list[str]: Paths to loader action plugins. From 055bf3fc179587de9c3affcabc93f63ad9f6aae2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:31:36 +0100 Subject: [PATCH 78/90] ignore locked containers when updating versions --- client/ayon_core/tools/sceneinventory/view.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 22bc170230..eb12fe5e06 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -1114,6 +1114,8 @@ class SceneInventoryView(QtWidgets.QTreeView): try: for item_id, item_version in zip(item_ids, versions): container = containers_by_id[item_id] + if container.get("version_locked"): + continue try: update_container(container, item_version) except Exception as exc: From 43b557d95e10978090492434ad8282dcd79d0958 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Mon, 1 Dec 2025 18:17:08 +0100 Subject: [PATCH 79/90] :recycle: check for compatibility --- client/ayon_core/pipeline/compatibility.py | 3 ++- client/ayon_core/pipeline/publish/lib.py | 6 ++--- client/ayon_core/plugins/publish/integrate.py | 25 ++++++++++++------- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/client/ayon_core/pipeline/compatibility.py b/client/ayon_core/pipeline/compatibility.py index f7d48526b7..dce627e391 100644 --- a/client/ayon_core/pipeline/compatibility.py +++ b/client/ayon_core/pipeline/compatibility.py @@ -13,4 +13,5 @@ def is_product_base_type_supported() -> bool: bool: True if product base types are supported, False otherwise. """ - return False + import ayon_api + return ayon_api.product_base_type_supported() diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index c2dcc89cd5..729e694c4f 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -122,8 +122,8 @@ def get_publish_template_name( task_type, project_settings=None, hero=False, + product_base_type: Optional[str] = None, logger=None, - product_base_type: Optional[str] = None ): """Get template name which should be used for passed context. @@ -141,10 +141,10 @@ def get_publish_template_name( task_type (str): Task type on which is instance working. project_settings (Dict[str, Any]): Prepared project settings. hero (bool): Template is for hero version publishing. - logger (logging.Logger): Custom logger used for 'filter_profiles' - function. product_base_type (Optional[str]): Product type for which should be found template. + logger (logging.Logger): Custom logger used for 'filter_profiles' + function. Returns: str: Template name which should be used for integration. diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 4589f6f542..eaf82b37c4 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -28,6 +28,7 @@ from ayon_core.pipeline.publish import ( KnownPublishError, get_publish_template_name, ) +from pipeline import is_product_base_type_supported log = logging.getLogger(__name__) @@ -396,15 +397,21 @@ class IntegrateAsset(pyblish.api.InstancePlugin): product_id = None if existing_product_entity: product_id = existing_product_entity["id"] - product_entity = new_product_entity( - product_name, - product_type, - folder_entity["id"], - data=data, - attribs=attributes, - entity_id=product_id, - product_base_type=product_base_type - ) + + new_product_entity_kwargs = { + "product_name": product_name, + "product_type": product_type, + "folder_id": folder_entity["id"], + "data": data, + "attribs": attributes, + "entity_id": product_id, + "product_base_type": product_base_type, + } + + if not is_product_base_type_supported(): + new_product_entity_kwargs.pop("product_base_type") + + product_entity = new_product_entity(**new_product_entity_kwargs) if existing_product_entity is None: # Create a new product From b0c5b171c9dec840dffb8f3dff88640198031598 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 1 Dec 2025 19:04:23 +0100 Subject: [PATCH 80/90] After OIIO transcoding force the layer name to be "" --- client/ayon_core/plugins/publish/extract_review.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 16fb22524c..54aa52c3c3 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -401,6 +401,10 @@ class ExtractReview(pyblish.api.InstancePlugin): new_staging_dir, self.log ) + # The OIIO conversion will remap the RGBA channels just to + # `R,G,B,A` so we will pass the intermediate file to FFMPEG + # without layer name. + layer_name = "" try: self._render_output_definitions( From 6ade0bb665e31afab07249eaaddc3b45eb497df0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 2 Dec 2025 14:44:29 +0100 Subject: [PATCH 81/90] Added webpublisher to extract thumbnail for ftrack --- client/ayon_core/plugins/publish/extract_thumbnail.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index 2a43c12af3..adfb4298b9 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -48,6 +48,7 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): "unreal", "houdini", "batchdelivery", + "webpublisher", ] settings_category = "core" enabled = False From 2efda3d3fec2745ef658b79a1f280d57177e827e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:41:37 +0100 Subject: [PATCH 82/90] :bug: fix import and function call/check --- client/ayon_core/pipeline/compatibility.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/pipeline/compatibility.py b/client/ayon_core/pipeline/compatibility.py index dce627e391..78ba5ad71e 100644 --- a/client/ayon_core/pipeline/compatibility.py +++ b/client/ayon_core/pipeline/compatibility.py @@ -1,4 +1,5 @@ """Package to handle compatibility checks for pipeline components.""" +import ayon_api def is_product_base_type_supported() -> bool: @@ -13,5 +14,7 @@ def is_product_base_type_supported() -> bool: bool: True if product base types are supported, False otherwise. """ - import ayon_api - return ayon_api.product_base_type_supported() + + if not hasattr(ayon_api, "is_product_base_type_supported"): + return False + return ayon_api.is_product_base_type_supported() From 1e6601786110e39f26ad26f7925471548cc45ab6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:47:39 +0100 Subject: [PATCH 83/90] :recycle: use product base type if defined when product base types are not supported by api, product base type should be the source of truth. --- client/ayon_core/plugins/publish/integrate.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index eaf82b37c4..e93cf62a3c 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -410,6 +410,18 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if not is_product_base_type_supported(): new_product_entity_kwargs.pop("product_base_type") + if ( + product_base_type is not None + and product_base_type != product_type): + self.log.warning(( + "Product base type %s is not supported by the server, " + "but it's defined - and it differs from product type %s. " + "Using product base type as product type." + ), product_base_type, product_type) + + new_product_entity_kwargs["product_type"] = ( + product_base_type + ) product_entity = new_product_entity(**new_product_entity_kwargs) From 206bcfe7176f3960bc764b5eee323c224aeeca6e Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:47:58 +0100 Subject: [PATCH 84/90] :memo: add warning --- client/ayon_core/pipeline/publish/lib.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index 729e694c4f..7365ffee09 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -149,6 +149,15 @@ def get_publish_template_name( Returns: str: Template name which should be used for integration. """ + if not product_base_type: + msg = ( + "Argument 'product_base_type' is not provided to" + " 'get_publish_template_name' function. This argument" + " will be required in future versions." + ) + warnings.warn(msg, DeprecationWarning) + if logger: + logger.warning(msg) template = None filter_criteria = { From a9c77857001bedb0b1f4dc5ec1b4e3b4599ac772 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Dec 2025 15:59:14 +0100 Subject: [PATCH 85/90] :dog: fix linter --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index e93cf62a3c..45209764ee 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -418,7 +418,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "but it's defined - and it differs from product type %s. " "Using product base type as product type." ), product_base_type, product_type) - + new_product_entity_kwargs["product_type"] = ( product_base_type ) From 7592bdcfcb99cfcc3e644330fe16e0bf694320f5 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 2 Dec 2025 16:39:48 +0000 Subject: [PATCH 86/90] [Automated] Add generated package files from main --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index 3e6b3794b1..a8e949f008 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.12+dev" +__version__ = "1.6.13" diff --git a/package.py b/package.py index fbf7021b8e..c94fd7527c 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.12+dev" +version = "1.6.13" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 208bbec85f..01994e7133 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.12+dev" +version = "1.6.13" description = "" authors = ["Ynput Team "] readme = "README.md" From 83c4350277e61b97f0d287092020ec8896ea4139 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Tue, 2 Dec 2025 16:40:29 +0000 Subject: [PATCH 87/90] [Automated] Update version in package.py for develop --- client/ayon_core/version.py | 2 +- package.py | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/version.py b/client/ayon_core/version.py index a8e949f008..e8b53001d5 100644 --- a/client/ayon_core/version.py +++ b/client/ayon_core/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring AYON addon 'core' version.""" -__version__ = "1.6.13" +__version__ = "1.6.13+dev" diff --git a/package.py b/package.py index c94fd7527c..003c41f0f5 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.13" +version = "1.6.13+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 01994e7133..b06f812b27 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.13" +version = "1.6.13+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 0ce6e705471f253804f0fa44289029194decd5b8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 2 Dec 2025 16:41:29 +0000 Subject: [PATCH 88/90] chore(): update bug report / version --- .github/ISSUE_TEMPLATE/bug_report.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 98e4b46e07..2b7756ba7e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -35,6 +35,7 @@ body: label: Version description: What version are you running? Look to AYON Tray options: + - 1.6.13 - 1.6.12 - 1.6.11 - 1.6.10 From 24ff7f02d695a48511f323e180268a20a0cf4176 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 4 Dec 2025 18:05:42 +0100 Subject: [PATCH 89/90] Fix wrongly resolved line --- client/ayon_core/pipeline/create/product_name.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 0cd3a1c51f..c4c27edc3b 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -54,7 +54,7 @@ def get_product_name_template( profiles = tools_settings["creator"]["product_name_profiles"] filtering_criteria = { "product_types": product_type, - "host_names": host_name,: host_name, + "host_names": host_name, "task_names": task_name, "task_types": task_type, "product_base_types": product_base_type, From f7f0005511c7aaa34e967100afa6db5f5ad53a1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 5 Dec 2025 12:23:30 +0100 Subject: [PATCH 90/90] Fix import --- client/ayon_core/plugins/publish/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 45209764ee..8fce5574e9 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -28,7 +28,7 @@ from ayon_core.pipeline.publish import ( KnownPublishError, get_publish_template_name, ) -from pipeline import is_product_base_type_supported +from ayon_core.pipeline import is_product_base_type_supported log = logging.getLogger(__name__)