🎨 use product name for render templates

this is fixing issue where product name templates were hardcoded. Old behavior is enabled by default but deprecation warning is issued
This commit is contained in:
Ondřej Samohel 2024-08-08 18:13:13 +02:00
parent 3b53093d12
commit 3eb960a856
No known key found for this signature in database
GPG key ID: 02376E18990A97C6
5 changed files with 237 additions and 75 deletions

View file

@ -1,7 +1,6 @@
import ayon_api
from ayon_core.lib import StringTemplate, filter_profiles, prepare_template_data
from ayon_core.settings import get_project_settings
from ayon_core.lib import filter_profiles, prepare_template_data
from .constants import DEFAULT_PRODUCT_TEMPLATE
@ -183,7 +182,10 @@ def get_product_name(
fill_pairs[key] = value
try:
return template.format(**prepare_template_data(fill_pairs))
return StringTemplate.format_template(
template=template,
data=prepare_template_data(fill_pairs)
)
except KeyError as exp:
raise TemplateFillError(
"Value for {} key is missing in template '{}'."

View file

@ -1,5 +1,5 @@
import os
import copy
import os
import re
import warnings
from copy import deepcopy
@ -7,14 +7,11 @@ from copy import deepcopy
import attr
import ayon_api
import clique
from ayon_core.pipeline import (
get_current_project_name,
get_representation_path,
)
from ayon_core.lib import Logger
from ayon_core.pipeline.publish import KnownPublishError
from ayon_core.pipeline import get_current_project_name, get_representation_path
from ayon_core.pipeline.create import get_product_name
from ayon_core.pipeline.farm.patterning import match_aov_pattern
from ayon_core.pipeline.publish import KnownPublishError
@attr.s
@ -250,6 +247,9 @@ def create_skeleton_instance(
"colorspace": data.get("colorspace")
}
if data.get("renderlayer"):
instance_skeleton_data["renderlayer"] = data["renderlayer"]
# skip locking version if we are creating v01
instance_version = data.get("version") # take this if exists
if instance_version != 1:
@ -464,7 +464,9 @@ def create_instances_for_aov(instance, skeleton, aov_filter,
Args:
instance (pyblish.api.Instance): Original instance.
skeleton (dict): Skeleton instance data.
aov_filter (dict): AOV filter.
skip_integration_repre_list (list): skip
do_not_add_review (bool): Explicitly disable reviews
Returns:
list of pyblish.api.Instance: Instances created from
@ -515,6 +517,113 @@ def create_instances_for_aov(instance, skeleton, aov_filter,
)
def _get_legacy_product_name_and_group(
product_type,
source_product_name,
task_name,
dynamic_data):
"""Get product name with legacy logic.
This function holds legacy behaviour of creating product name
that is deprecated. This wasn't using product name templates
at all, only hardcoded values. It shouldn't be used anymore,
but transition to templates need careful checking of the project
and studio settings.
Deprecated:
since 0.4.4
Args:
product_type (str): Product type.
source_product_name (str): Source product name.
task_name (str): Task name.
dynamic_data (dict): Dynamic data (camera, aov, ...)
Returns:
tuple: product name and group name
"""
warnings.warn("Using legacy product name for renders",
DeprecationWarning)
if not source_product_name.startswith(product_type):
resulting_group_name = '{}{}{}{}{}'.format(
product_type,
task_name[0].upper(), task_name[1:],
source_product_name[0].upper(), source_product_name[1:])
else:
resulting_group_name = source_product_name
# create product name `<product type><Task><Product name>`
if not source_product_name.startswith(product_type):
resulting_group_name = '{}{}{}{}{}'.format(
product_type,
task_name[0].upper(), task_name[1:],
source_product_name[0].upper(), source_product_name[1:])
else:
resulting_group_name = source_product_name
resulting_product_name = '{}'.format(resulting_group_name)
camera = dynamic_data.get("camera")
aov = dynamic_data.get("aov")
if camera:
if not aov:
resulting_product_name = '{}_{}'.format(
resulting_group_name, camera)
elif not aov.startswith(camera):
resulting_product_name = '{}_{}_{}'.format(
resulting_group_name, camera, aov)
else:
resulting_product_name = "{}_{}".format(
resulting_group_name, aov)
else:
if aov:
resulting_product_name = '{}_{}'.format(
resulting_group_name, aov)
return resulting_product_name, resulting_group_name
def _get_product_name_and_group_from_template(
task_entity,
project_name,
host_name,
product_type,
variant,
project_settings,
dynamic_data=None):
"""Get product name and group name from template.
This will get product name and group name from template based on
data provided. It is doing similar work as
`func::_get_legacy_product_name_and_group` but using templates.
Args:
task_entity (dict): Task entity.
project_name (str): Project name.
host_name (str): Host name.
product_type (str): Product type.
variant (str): Variant.
project_settings (dict): Project settings.
dynamic_data (dict): Dynamic data (aov, renderlayer, camera, ...).
Returns:
tuple: product name and group name.
"""
resulting_product_name = get_product_name(
project_name=project_name,
task_name=task_entity["taskName"],
task_type=task_entity["taskType"],
host_name=host_name,
product_type=product_type,
dynamic_data=dynamic_data,
variant=variant,
project_settings=project_settings,
)
return resulting_product_name, ""
def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
skip_integration_repre_list, do_not_add_review):
"""Create instance for each AOV found.
@ -526,10 +635,10 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
instance (pyblish.api.Instance): Original instance.
skeleton (dict): Skeleton data for instance (those needed) later
by collector.
additional_data (dict): ..
additional_data (dict): ...
skip_integration_repre_list (list): list of extensions that shouldn't
be published
do_not_addbe _review (bool): explicitly disable review
do_not_add_review (bool): explicitly disable review
Returns:
@ -539,68 +648,69 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
ValueError:
"""
# TODO: this needs to be taking the task from context or instance
task = os.environ["AYON_TASK_NAME"]
anatomy = instance.context.data["anatomy"]
s_product_name = skeleton["productName"]
source_product_name = skeleton["productName"]
cameras = instance.data.get("cameras", [])
exp_files = instance.data["expectedFiles"]
expected_files = instance.data["expectedFiles"]
log = Logger.get_logger("farm_publishing")
instances = []
# go through AOVs in expected files
for aov, files in exp_files[0].items():
cols, rem = clique.assemble(files)
# we shouldn't have any reminders. And if we do, it should
# be just one item for single frame renders.
if not cols and rem:
if len(rem) != 1:
raise ValueError("Found multiple non related files "
"to render, don't know what to do "
"with them.")
col = rem[0]
ext = os.path.splitext(col)[1].lstrip(".")
else:
# but we really expect only one collection.
# Nothing else make sense.
if len(cols) != 1:
raise ValueError("Only one image sequence type is expected.") # noqa: E501
ext = cols[0].tail.lstrip(".")
col = list(cols[0])
for aov, files in expected_files[0].items():
collected_files = _collect_expected_files_for_aov(files)
# create product name `<product type><Task><Product name>`
# TODO refactor/remove me
product_type = skeleton["productType"]
if not s_product_name.startswith(product_type):
group_name = '{}{}{}{}{}'.format(
product_type,
task[0].upper(), task[1:],
s_product_name[0].upper(), s_product_name[1:])
else:
group_name = s_product_name
expected_filepath = collected_files[0] \
if isinstance(collected_files, (list, tuple)) else collected_files
# if there are multiple cameras, we need to add camera name
expected_filepath = col[0] if isinstance(col, (list, tuple)) else col
cams = [cam for cam in cameras if cam in expected_filepath]
if cams:
for cam in cams:
if not aov:
product_name = '{}_{}'.format(group_name, cam)
elif not aov.startswith(cam):
product_name = '{}_{}_{}'.format(group_name, cam, aov)
else:
product_name = "{}_{}".format(group_name, aov)
else:
if aov:
product_name = '{}_{}'.format(group_name, aov)
else:
product_name = '{}'.format(group_name)
dynamic_data = {
"aov": aov,
"renderlayer": instance.data.get("renderlayer"),
}
# find if camera is used in the file path
# TODO: this must be changed to be more robust. Any coincidence
# of camera name in the file path will be considered as
# camera name. This is not correct.
camera = [cam for cam in cameras if cam in expected_filepath]
# Is there just one camera matching?
# TODO: this is not true, we can have multiple cameras in the scene
# and we should be able to detect them all. Currently, we are
# keeping the old behavior, taking the first one found.
if camera:
dynamic_data["camera"] = camera[0]
project_settings = instance.context.data.get("project_settings")
use_legacy_product_name = True
try:
use_legacy_product_name = project_settings["core"]["tools"]["creator"]["product_name_profiles"]["use_legacy_for_renders"] # noqa: E501
except KeyError:
warnings.warn(
("use_legacy_for_renders not found in project settings. "
"Using legacy product name for renders. Please update "
"your ayon-core version."), DeprecationWarning)
use_legacy_product_name = True
if use_legacy_product_name:
product_name, group_name = _get_legacy_product_name_and_group(
product_type=skeleton["productType"],
source_product_name=source_product_name,
task_name=instance.data["task"],
dynamic_data=dynamic_data)
if isinstance(col, (list, tuple)):
staging = os.path.dirname(col[0])
else:
staging = os.path.dirname(col)
product_name, group_name = _get_product_name_and_group_from_template(
task_entity=instance.data["taskEntity"],
project_name=instance.context.data["projectName"],
host_name=instance.context.data["hostName"],
product_type=skeleton["productType"],
variant=instance.data.get('variant', ''),
project_settings=project_settings
)
staging = os.path.dirname(expected_filepath)
try:
staging = remap_source(staging, anatomy)
@ -611,10 +721,8 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
app = os.environ.get("AYON_HOST_NAME", "")
if isinstance(col, list):
render_file_name = os.path.basename(col[0])
else:
render_file_name = os.path.basename(col)
render_file_name = os.path.basename(expected_filepath)
aov_patterns = aov_filter
preview = match_aov_pattern(app, aov_patterns, render_file_name)
@ -622,9 +730,10 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
new_instance = deepcopy(skeleton)
new_instance["productName"] = product_name
new_instance["productGroup"] = group_name
new_instance["aov"] = aov
# toggle preview on if multipart is on
# Because we cant query the multipartExr data member of each AOV we'll
# Because we can't query the multipartExr data member of each AOV we'll
# need to have hardcoded rule of excluding any renders with
# "cryptomatte" in the file name from being a multipart EXR. This issue
# happens with Redshift that forces Cryptomatte renders to be separate
@ -650,10 +759,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
new_instance["review"] = True
# create representation
if isinstance(col, (list, tuple)):
files = [os.path.basename(f) for f in col]
else:
files = os.path.basename(col)
ext = os.path.splitext(render_file_name)[-1].lstrip(".")
# Copy render product "colorspace" data to representation.
colorspace = ""
@ -708,6 +814,36 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data,
return instances
def _collect_expected_files_for_aov(files):
"""Collect expected files.
Args:
files (list): List of files.
Returns:
list or str: Collection of files or single file.
Raises:
ValueError: If there are multiple collections.
"""
cols, rem = clique.assemble(files)
# we shouldn't have any reminders. And if we do, it should
# be just one item for single frame renders.
if not cols and rem:
if len(rem) != 1:
raise ValueError("Found multiple non related files "
"to render, don't know what to do "
"with them.")
return rem[0]
else:
# but we really expect only one collection.
# Nothing else make sense.
if len(cols) != 1:
raise ValueError("Only one image sequence type is expected.") # noqa: E501
return list(cols[0])
def get_resources(project_name, version_entity, extension=None):
"""Get the files from the specific version.

View file

@ -138,7 +138,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
folder_path_by_id = {}
for instance in context:
folder_entity = instance.data.get("folderEntity")
# Skip if instnace does not have filled folder entity
# Skip if instance does not have filled folder entity
if not folder_entity:
continue
folder_id = folder_entity["id"]
@ -385,8 +385,19 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
json.dumps(anatomy_data, indent=4)
))
# make render layer available in anatomy data
render_layer = instance.data.get("renderLayer")
if render_layer:
anatomy_data["renderLayer"] = render_layer
# make aov name available in anatomy data
aov = instance.data.get("aov")
if aov:
anatomy_data["aov"] = aov
def _fill_folder_data(self, instance, project_entity, anatomy_data):
# QUESTION should we make sure that all folder data are poped if
# QUESTION: should we make sure that all folder data are popped if
# folder data cannot be found?
# - 'folder', 'hierarchy', 'parent', 'folder'
folder_entity = instance.data.get("folderEntity")
@ -426,7 +437,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin):
})
def _fill_task_data(self, instance, task_types_by_name, anatomy_data):
# QUESTION should we make sure that all task data are poped if task
# QUESTION: should we make sure that all task data are popped if task
# data cannot be resolved?
# - 'task'

View file

@ -744,6 +744,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin):
if not is_udim:
repre_context["frame"] = first_index_padded
# store renderlayer in context if it exists
# to be later used for example by delivery templates
if instance.data.get("renderlayer"):
repre_context["renderlayer"] = instance.data["renderlayer"]
# Update the destination indexes and padding
dst_collection = clique.assemble(dst_filepaths)[0][0]
dst_collection.padding = destination_padding

View file

@ -22,6 +22,14 @@ class ProductTypeSmartSelectModel(BaseSettingsModel):
class ProductNameProfile(BaseSettingsModel):
_layout = "expanded"
# TODO: change to False in next releases
use_legacy_for_renders: bool = SettingsField(
True, title="Use legacy for renders",
description="Use product naming logic for renders. "
"This is for backwards compatibility enabled by default."
"When enabled, it will ignore any templates for renders "
"that are set in the product name profiles.")
product_types: list[str] = SettingsField(
default_factory=list, title="Product types"
)