Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 36 additions & 7 deletions client/ayon_core/pipeline/delivery.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ def check_destination_path(
anatomy,
anatomy_data,
datetime_data,
template_name
template_name,
*,
explicit_template_obj=None,
):
""" Try to create destination path based on 'template_name'.

Expand All @@ -80,14 +82,20 @@ def check_destination_path(
datetime_data (dict): Values with actual date.
template_name (str): Name of template which should be used from anatomy
templates.
explicit_template_obj (AnatomyTemplateItem, optional): Anatomy template
item to use instead of the one defined in the delivery template.
Returns:
Dict[str, List[str]]: Report of happened errors. Key is message title
value is detailed information.
"""

anatomy_data.update(datetime_data)
path_template = anatomy.get_template_item(
"delivery", template_name, "path"

if explicit_template_obj is not None:
path_template = explicit_template_obj.get("path")
else:
path_template = anatomy.get_template_item(
"delivery", template_name, "path"
)
dest_path = path_template.format(anatomy_data)
report_items = collections.defaultdict(list)
Expand Down Expand Up @@ -131,7 +139,9 @@ def deliver_single_file(
anatomy_data,
format_dict,
report_items,
log
log,
*,
explicit_template_obj=None
):
"""Copy single file to calculated path based on template

Expand All @@ -145,6 +155,8 @@ def deliver_single_file(
format_dict (dict): root dictionary with names and values
report_items (collections.defaultdict): to return error messages
log (logging.Logger): for log printing
explicit_template_obj (AnatomyTemplateItem, optional): Anatomy template
item to use instead of the one defined in the delivery template.

Returns:
(collections.defaultdict, int)
Expand All @@ -161,8 +173,12 @@ def deliver_single_file(
if format_dict:
anatomy_data = copy.deepcopy(anatomy_data)
anatomy_data["root"] = format_dict["root"]
template_obj = anatomy.get_template_item(
"delivery", template_name, "path"

if explicit_template_obj is not None:
template_obj = explicit_template_obj.get("path")
else:
template_obj = anatomy.get_template_item(
"delivery", template_name, "path"
)
delivery_path = template_obj.format_strict(anatomy_data)

Expand All @@ -174,6 +190,16 @@ def deliver_single_file(
delivery_path = delivery_path.rstrip()

delivery_folder = os.path.dirname(delivery_path)
# Remove frame number if is find in folder path
# usually due publishedFilename token used in directory
frame = anatomy_data.get("frame")
if frame is not None:
frame_pattern = f".{frame}"
if frame_pattern in delivery_folder:
delivery_folder = delivery_folder.replace(frame_pattern, "")
delivery_path = os.path.join(
delivery_folder, os.path.basename(delivery_path))
Comment on lines +193 to +201

if not os.path.exists(delivery_folder):
os.makedirs(delivery_folder)

Expand Down Expand Up @@ -213,6 +239,9 @@ def deliver_sequence(
format_dict (dict): root dictionary with names and values
report_items (collections.defaultdict): to return error messages
log (logging.Logger): for log printing
has_renumbered_frame (bool, optional): whether the frame has been
renumbered.
new_frame_start (int, optional): new frame start value.

Returns:
(collections.defaultdict, int)
Expand All @@ -234,7 +263,7 @@ def hash_path_exist(myPath):
return report_items, 0

delivery_template = anatomy.get_template_item(
"delivery", template_name, "path", default=None
"delivery", template_name, "path", default=None
)
if delivery_template is None:
msg = (
Expand Down
109 changes: 98 additions & 11 deletions client/ayon_core/tools/delivery/delivery.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
import logging
import os
import platform
import logging
from collections import defaultdict

import ayon_api
from qtpy import QtWidgets, QtCore, QtGui
from qtpy import QtCore, QtGui, QtWidgets

from ayon_core import resources, style
from ayon_core.lib import (
format_file_size,
collect_frames,
format_file_size,
get_datetime_data,
)
from ayon_core.pipeline import Anatomy

from ayon_core.pipeline.load import get_representation_path_with_anatomy
from ayon_core.pipeline.anatomy.templates import TemplateItem
from ayon_core.pipeline.delivery import (
get_format_dict,
check_destination_path,
deliver_single_file,
get_format_dict,
get_representations_delivery_template_data,
)
from ayon_core.pipeline.load import get_representation_path_with_anatomy
from ayon_core.settings import get_project_settings


class DeliveryOptionsDialog(QtWidgets.QDialog):
Expand Down Expand Up @@ -229,7 +230,18 @@ def deliver(self):
self.anatomy.project_name, repre_ids
)
)
core_settings = get_project_settings(self.anatomy.project_name)
overrides: list[dict] = (
core_settings["core"]["tools"]["delivery"]["overrides"]
)
override_preset = None
for preset in overrides:
if preset["name"] == template_name:
override_preset = preset
break

for repre in filtered_repres:
explicit_template_obj = None
repre_path = get_representation_path_with_anatomy(
repre, self.anatomy
)
Expand All @@ -238,6 +250,11 @@ def deliver(self):
if list_label:
template_data["list"] = {"label": list_label}

if override_preset:
explicit_template_obj = self._get_explicit_template_obj(
repre, override_preset, template_name
)

# Use temporary placeholder so we don't need to check destination
# path per file of the representation
template_data["publishedFilename"] = "<publishedFilename>"
Expand All @@ -246,7 +263,8 @@ def deliver(self):
self.anatomy,
template_data,
datetime_data,
template_name
template_name,
explicit_template_obj=explicit_template_obj,
)

report_items.update(new_report_items)
Expand All @@ -261,7 +279,7 @@ def deliver(self):
template_data,
format_dict,
report_items,
self.log
self.log,
]

# TODO: This will currently incorrectly detect 'resources'
Expand All @@ -283,11 +301,13 @@ def deliver(self):

for src_path, frame in sources_and_frames.items():
# Support {publishedFilename} token
template_data["publishedFilename"] = os.path.basename(
publish_basename = os.path.basename(

@iLLiCiTiT iLLiCiTiT Jul 21, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this? This is supposed to be filename, not basename. Filename has extension. It doesn't make sense to change extension of a file during delivery.

# Replace backslash to forward slashes so basename
# also resolves to filename only on POSIX
src_path.replace("\\", "/")
)
filename, _ = os.path.splitext(publish_basename)
template_data["publishedFilename"] = filename
args[0] = src_path
Comment on lines +309 to 311
# Renumber frames
if renumber_frame and frame is not None:
Expand All @@ -298,7 +318,10 @@ def deliver(self):
# Add offset to new frame start
dst_frame = int(frame) + offset
if dst_frame < 0:
msg = "Renumber frame has a smaller number than original frame" # noqa
msg = (
"Renumber frame has a smaller number than "
"original frame"
)
report_items[msg].append(src_path)
self.log.warning("{} <{}>".format(
msg, dst_frame))
Expand All @@ -318,13 +341,77 @@ def deliver(self):
" formatting data."
)
template_data["frame"] = frame
new_report_items, uploaded = deliver_single_file(*args)

new_report_items, uploaded = deliver_single_file(
*args,
explicit_template_obj=explicit_template_obj
)
report_items.update(new_report_items)
self._update_progress(uploaded)

self.text_area.setText(self._format_report(report_items))
self.text_area.setVisible(True)

def _get_explicit_template_obj(
self, repre, override_preset, template_name
):
"""Build an explicit template override for a representation."""
if not override_preset:
return None

matching_rule = None
for rule in override_preset["rules"]:
if rule["name"] == repre["name"]:
matching_rule = rule
break

if matching_rule is None:
return None

original_template = self.templates[template_name]
if (
not matching_rule["template_dir"]
and not matching_rule["template_file"]
):
return None

original_directory = original_template["directory"]
original_file = original_template["file"]
# make sure if file or directory is in template it is
# wrapped into double braces so it is not formatted
# e.g. "{directory}" should be "{{directory}}" so it is not
# replaced by the format method
Comment on lines +380 to +383
m_directory_tmpl = (
matching_rule["template_dir"] or original_directory
)
if m_directory_tmpl != original_directory:
if "{directory}" in m_directory_tmpl:
m_directory_tmpl = m_directory_tmpl.replace(
"{directory}", original_directory
)
if "{file}" in m_directory_tmpl:
m_directory_tmpl = m_directory_tmpl.replace(
"{file}", original_file
)

m_file_tmpl = (
matching_rule["template_file"] or original_file
)
if m_file_tmpl != original_file:
if "{directory}" in m_file_tmpl:
m_file_tmpl = m_file_tmpl.replace(
"{directory}", original_directory
)
if "{file}" in m_file_tmpl:
m_file_tmpl = m_file_tmpl.replace(
"{file}", original_file
)

return TemplateItem(
self.anatomy.templates_obj,
{"directory": m_directory_tmpl, "file": m_file_tmpl},
)

def _get_representation_names(self):
"""Get set of representation names for checkbox filtering."""
return set([repre["name"] for repre in self._representations])
Expand Down
73 changes: 72 additions & 1 deletion server/settings/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -451,6 +451,69 @@ class PublishToolModel(BaseSettingsModel):
)


class DeliveryRepresentationRuleModel(BaseSettingsModel):
name: str = SettingsField(
"",
title="Representation Name",
description="Name of the representation to match"
)
template_dir: str = SettingsField(
"",
title="Directory Template Override",
description=(
"Overriding 'Directory Template'. Additional tokens: \n"
"- `{directory}` - original 'Directory Template'\n"
"- `{file}` - original 'File Name Template'"
)
)
template_file: str = SettingsField(
"",
title="File Name Template Override",
description=(
"Overriding 'File Name Template'. Additional tokens: \n"
"- `{directory}` - original 'Directory Template'\n"
"- `{file}` - original 'File Name Template'"
)
)

@validator("name")
def normalize_value(cls, value):
return normalize_name(value)


class DeliveryTemplateOverrideModel(BaseSettingsModel):
name: str = SettingsField(
"",
title="Anatomy DeliveryTemplate Name",
description="Name of the delivery category template to match"
)
rules: list[DeliveryRepresentationRuleModel] = SettingsField(
default_factory=list,
title="Representation Rules",
description=(
"Rules to override the chosen delivery template. "
"Rules are evaluated in order, and the first matching rule "
"determines the delivery template to use."
)
)

@validator("name")
def normalize_value(cls, value):
return normalize_name(value)


class DeliveryToolModel(BaseSettingsModel):
overrides: list[DeliveryTemplateOverrideModel] = SettingsField(
default_factory=list,
title="Delivery Anatomy Template Override Presets",
description=(
"Overrides presets to override the chosen delivery template. "
"Evaluated in order, and the first matching override "
"determines the delivery template to use."
)
)


class GlobalToolsModel(BaseSettingsModel):
ayon_menu: AYONMenuModel = SettingsField(
default_factory=AYONMenuModel,
Expand All @@ -476,6 +539,10 @@ class GlobalToolsModel(BaseSettingsModel):
default_factory=PublishToolModel,
title="Publish"
)
delivery: DeliveryToolModel = SettingsField(
default_factory=DeliveryToolModel,
title="Delivery"
)


DEFAULT_TOOLS_VALUES = {
Expand Down Expand Up @@ -772,5 +839,9 @@ class GlobalToolsModel(BaseSettingsModel):
"ignore_paths": [],
},
"comment_minimum_required_chars": 0,
}
},
"delivery": {
"enabled": False,
"overrides": [],
},
Comment on lines +843 to +846
}
Loading