From 148ce21a9ac25a446a3ce2233501d288b396d34b Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:09:12 +0100 Subject: [PATCH 01/95] Allow ExtractOIIOTranscode to pass with a warning if it can't find the RGBA channels in source media instead of raising error --- client/ayon_core/lib/transcoding.py | 7 ++++- .../publish/extract_color_transcode.py | 30 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e9750864ac..7fe2c84789 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -67,6 +67,11 @@ VIDEO_EXTENSIONS = { } +class UnknownRGBAChannelsError(ValueError): + """Raised when we can't find RGB channels for conversion in input media.""" + pass + + def get_transcode_temp_directory(): """Creates temporary folder for transcoding. @@ -1427,7 +1432,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: - raise ValueError( + raise UnknownRGBAChannelsError( "Couldn't find channels that can be used for conversion." ) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 3e54d324e3..b259a3230a 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -10,6 +10,7 @@ from ayon_core.lib import ( ) from ayon_core.lib.transcoding import ( + UnknownRGBAChannelsError, convert_colorspace, get_transcode_temp_directory, ) @@ -160,17 +161,24 @@ class ExtractOIIOTranscode(publish.Extractor): output_path = self._get_output_file_path(input_path, new_staging_dir, output_extension) - convert_colorspace( - input_path, - output_path, - config_path, - source_colorspace, - target_colorspace, - view, - display, - additional_command_args, - self.log - ) + try: + convert_colorspace( + input_path, + output_path, + config_path, + source_colorspace, + target_colorspace, + view, + display, + additional_command_args, + self.log + ) + except UnknownRGBAChannelsError: + self.log.error( + "Skipping OIIO Transcode. Unknown RGBA channels" + f" for colorspace conversion in file: {input_path}" + ) + continue # cleanup temporary transcoded files for file_name in new_repre["files"]: From d072da86d1abada39af3e024c6010efc5554cd52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:11:22 +0100 Subject: [PATCH 02/95] Stop processing directly on unknown RGBA channel for a representation --- .../plugins/publish/extract_color_transcode.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index b259a3230a..0287eca300 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -153,8 +153,7 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args = (output_def["oiiotool_args"] ["additional_command_args"]) - files_to_convert = self._translate_to_sequence( - files_to_convert) + unknown_rgba_channels = False for file_name in files_to_convert: input_path = os.path.join(original_staging_dir, file_name) @@ -174,11 +173,16 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) except UnknownRGBAChannelsError: + unknown_rgba_channels = True self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" f" for colorspace conversion in file: {input_path}" ) - continue + break + + if unknown_rgba_channels: + # Stop processing this representation + break # cleanup temporary transcoded files for file_name in new_repre["files"]: From 363824d58946796d148b0216a5464928a91b3b1d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 6 Nov 2024 09:12:19 +0100 Subject: [PATCH 03/95] Move logic to make it clearer that we always process the same input files of the source representation but used for multiple output profiles --- .../plugins/publish/extract_color_transcode.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 0287eca300..0d88f3073f 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -100,7 +100,19 @@ class ExtractOIIOTranscode(publish.Extractor): self.log.warning("Config file doesn't exist, skipping") 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"]] + repre_files_to_convert = self._translate_to_sequence( + repre_files_to_convert) + + # 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) @@ -108,11 +120,6 @@ class ExtractOIIOTranscode(publish.Extractor): new_staging_dir = get_transcode_temp_directory() new_repre["stagingDir"] = new_staging_dir - if isinstance(new_repre["files"], list): - files_to_convert = copy.deepcopy(new_repre["files"]) - else: - files_to_convert = [new_repre["files"]] - output_extension = output_def["extension"] output_extension = output_extension.replace('.', '') self._rename_in_representation(new_repre, @@ -121,7 +128,6 @@ class ExtractOIIOTranscode(publish.Extractor): output_extension) transcoding_type = output_def["transcoding_type"] - target_colorspace = view = display = None # NOTE: we use colorspace_data as the fallback values for # the target colorspace. From 2d7bd487bac24b28c78e5bf67a19c4f69c6e3db7 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 13 May 2025 15:40:40 +0200 Subject: [PATCH 04/95] Allow review/transcoding of more channels, like "Y", "XYZ", "AR", "AG", "AB" --- client/ayon_core/lib/transcoding.py | 73 ++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 18 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1fda014bd8..b236cd101b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -345,6 +345,9 @@ def get_review_info_by_layer_name(channel_names): ... ] + This tries to find suitable outputs good for review purposes, by + searching for channel names like 'red', 'green', 'blue' or 'R', 'G', 'B', + Args: channel_names (list[str]): List of channel names. @@ -353,7 +356,6 @@ def get_review_info_by_layer_name(channel_names): """ layer_names_order = [] - rgba_by_layer_name = collections.defaultdict(dict) channels_by_layer_name = collections.defaultdict(dict) for channel_name in channel_names: @@ -362,42 +364,76 @@ def get_review_info_by_layer_name(channel_names): if "." in channel_name: layer_name, last_part = channel_name.rsplit(".", 1) - channels_by_layer_name[layer_name][channel_name] = last_part if last_part.lower() not in { - "r", "red", - "g", "green", - "b", "blue", - "a", "alpha" + # Detect RGBA channels + "r", "g", "b", "a", + # Allow detecting of x, y and z channels, and normal channels + "x", "y", "z", "n", + # red, green and blue alpha/opacity, for colored mattes + "ar", "ag", "ab" }: continue if layer_name not in layer_names_order: layer_names_order.append(layer_name) - # R, G, B or A + + # R, G, B, A or X, Y, Z, N, AR, AG, AB channel = last_part[0].upper() - rgba_by_layer_name[layer_name][channel] = channel_name + channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list # - if input has R, G, B, A channels they should be used for review - if "" in layer_names_order: - layer_names_order.remove("") - layer_names_order.insert(0, "") + def _sort(_layer_name: str) -> int: + # Prioritize "" layer name + # Prioritize layers with RGB channels + order = 0 + if _layer_name == "": + order -= 10 + + channels = channels_by_layer_name[_layer_name] + if all(channel in channels for channel in "RGB"): + order -= 1 + return order + layer_names_order.sort(key=_sort) output = [] for layer_name in layer_names_order: - rgba_layer_info = rgba_by_layer_name[layer_name] - red = rgba_layer_info.get("R") - green = rgba_layer_info.get("G") - blue = rgba_layer_info.get("B") - if not red or not green or not blue: + channel_info = channels_by_layer_name[layer_name] + + # RGB channels + if all(channel in channel_info for channel in "RGB"): + rgb = "R", "G", "B" + + # XYZ channels (position pass) + elif all(channel in channel_info for channel in "XYZ"): + rgb = "X", "Y", "Z" + + # Colored mattes (as defined in OpenEXR Channel Name standards) + elif all(channel in channel_info for channel in ("AR", "AG", "AB")): + rgb = "AR", "AG", "AB" + + # Luminance channel (as defined in OpenEXR Channel Name standards) + elif "Y" in channel_info: + rgb = "Y", "Y", "Y" + + # Has only Z channel (Z-depth layer) + elif "Z" in channel_info: + rgb = "Z", "Z", "Z" + + else: + # No reviewable channels found continue + + red = channel_info[rgb[0]] + green = channel_info[rgb[1]] + blue = channel_info[rgb[2]] output.append({ "name": layer_name, "review_channels": { "R": red, "G": green, "B": blue, - "A": rgba_layer_info.get("A"), + "A": channel_info.get("A"), } }) return output @@ -1428,7 +1464,8 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): if review_channels is None: raise ValueError( - "Couldn't find channels that can be used for conversion." + "Couldn't find channels that can be used for conversion " + f"among channels: {channel_names}." ) red, green, blue, alpha = review_channels From afbf2c8848b6045dd7d712323237a7d823922d5f Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:40:54 +0200 Subject: [PATCH 05/95] Refactor `UnknownRGBAChannelsError` -> `MissingRGBAChannelsError` --- client/ayon_core/lib/transcoding.py | 4 ++-- client/ayon_core/plugins/publish/extract_color_transcode.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 8c96bde07e..07b6d8a039 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -67,7 +67,7 @@ VIDEO_EXTENSIONS = { } -class UnknownRGBAChannelsError(ValueError): +class MissingRGBAChannelsError(ValueError): """Raised when we can't find RGB channels for conversion in input media.""" pass @@ -1337,7 +1337,7 @@ def get_oiio_input_and_channel_args(oiio_input_info, alpha_default=None): review_channels = get_convert_rgb_channels(channel_names) if review_channels is None: - raise UnknownRGBAChannelsError( + raise MissingRGBAChannelsError( "Couldn't find channels that can be used for conversion " f"among channels: {channel_names}." ) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 0475b63f82..a4b6fe7fc2 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -11,7 +11,7 @@ from ayon_core.lib import ( is_oiio_supported, ) from ayon_core.lib.transcoding import ( - UnknownRGBAChannelsError, + MissingRGBAChannelsError, convert_colorspace, ) @@ -186,7 +186,7 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args, self.log ) - except UnknownRGBAChannelsError: + except MissingRGBAChannelsError: unknown_rgba_channels = True self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" From 44dc1ea99e46c108bfbc9f4dd63c9eff7a945731 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:43:41 +0200 Subject: [PATCH 06/95] Include message of the original raised error --- client/ayon_core/plugins/publish/extract_color_transcode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index a4b6fe7fc2..09f7e6f201 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -186,8 +186,9 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args, self.log ) - except MissingRGBAChannelsError: + except MissingRGBAChannelsError as exc: unknown_rgba_channels = True + self.log.error(exc) self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" f" for colorspace conversion in file: {input_path}" From 7fa192229c207483c3bf30bf55d479a1d2aae8b3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Sat, 17 May 2025 15:47:50 +0200 Subject: [PATCH 07/95] Improve docstring --- client/ayon_core/lib/transcoding.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 07b6d8a039..e665486d7b 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -351,7 +351,8 @@ def get_review_info_by_layer_name(channel_names): ] This tries to find suitable outputs good for review purposes, by - searching for channel names like 'red', 'green', 'blue' or 'R', 'G', 'B', + searching for channel names like RGBA, but also XYZ, Z, N, AR, AG, AB + channels. Args: channel_names (list[str]): List of channel names. From 72895df6ae121b013c63071e080e2609cc22b7a0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 11:16:52 +0200 Subject: [PATCH 08/95] Match variable name more with captured exception --- client/ayon_core/plugins/publish/extract_color_transcode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 09f7e6f201..45832bd1e5 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -166,7 +166,7 @@ class ExtractOIIOTranscode(publish.Extractor): files_to_convert = self._translate_to_sequence( files_to_convert) self.log.debug("Files to convert: {}".format(files_to_convert)) - unknown_rgba_channels = False + missing_rgba_review_channels = False for file_name in files_to_convert: self.log.debug("Transcoding file: `{}`".format(file_name)) input_path = os.path.join(original_staging_dir, @@ -187,7 +187,7 @@ class ExtractOIIOTranscode(publish.Extractor): self.log ) except MissingRGBAChannelsError as exc: - unknown_rgba_channels = True + missing_rgba_review_channels = True self.log.error(exc) self.log.error( "Skipping OIIO Transcode. Unknown RGBA channels" @@ -195,7 +195,7 @@ class ExtractOIIOTranscode(publish.Extractor): ) break - if unknown_rgba_channels: + if missing_rgba_review_channels: # Stop processing this representation break From b8ea018b43b7256676362f699d6f66df848cb710 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 11:18:37 +0200 Subject: [PATCH 09/95] Clarify exception --- client/ayon_core/lib/transcoding.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index e665486d7b..57ae6cb0bc 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -68,7 +68,12 @@ VIDEO_EXTENSIONS = { class MissingRGBAChannelsError(ValueError): - """Raised when we can't find RGB channels for conversion in input media.""" + """Raised when we can't find channels to use as RGBA for conversion in + input media. + + This may be other channels than solely RGBA, like Z-channel. The error is + raised when no matching 'reviewable' channel was found. + """ pass From 526e5bfabb2af995863ef652fb34635002f37641 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:01:14 +0200 Subject: [PATCH 10/95] Add unittest --- .../client/ayon_core/lib/test_transcoding.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 tests/client/ayon_core/lib/test_transcoding.py diff --git a/tests/client/ayon_core/lib/test_transcoding.py b/tests/client/ayon_core/lib/test_transcoding.py new file mode 100644 index 0000000000..f637e8bd9e --- /dev/null +++ b/tests/client/ayon_core/lib/test_transcoding.py @@ -0,0 +1,135 @@ +import unittest + +from ayon_core.lib.transcoding import ( + get_review_info_by_layer_name +) + + +class GetReviewInfoByLayerName(unittest.TestCase): + """Test responses from `get_review_info_by_layer_name`""" + def test_rgba_channels(self): + + # RGB is supported + info = get_review_info_by_layer_name(["R", "G", "B"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": None, + } + }]) + + # rgb is supported + info = get_review_info_by_layer_name(["r", "g", "b"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "r", + "G": "g", + "B": "b", + "A": None, + } + }]) + + # diffuse.[RGB] is supported + info = get_review_info_by_layer_name( + ["diffuse.R", "diffuse.G", "diffuse.B"] + ) + self.assertEqual(info, [{ + "name": "diffuse", + "review_channels": { + "R": "diffuse.R", + "G": "diffuse.G", + "B": "diffuse.B", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["R", "G", "B", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": "A", + } + }]) + + def test_z_channel(self): + + info = get_review_info_by_layer_name(["Z"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "Z", + "G": "Z", + "B": "Z", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["Z", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "Z", + "G": "Z", + "B": "Z", + "A": "A", + } + }]) + + + def test_unknown_channels(self): + info = get_review_info_by_layer_name(["hello", "world"]) + self.assertEqual(info, []) + + def test_rgba_priority(self): + """Ensure main layer, and RGB channels are prioritized + + If both Z and RGB channels are present for a layer name, then RGB + should be prioritized and the Z channel should be ignored. + + Also, the alpha channel from another "layer name" is not used. Note + how the diffuse response does not take A channel from the main layer. + + """ + + info = get_review_info_by_layer_name([ + "Z", + "diffuse.R", "diffuse.G", "diffuse.B", + "R", "G", "B", "A", + "specular.R", "specular.G", "specular.B", "specular.A", + ]) + self.assertEqual(info, [ + { + "name": "", + "review_channels": { + "R": "R", + "G": "G", + "B": "B", + "A": "A", + }, + }, + { + "name": "diffuse", + "review_channels": { + "R": "diffuse.R", + "G": "diffuse.G", + "B": "diffuse.B", + "A": None, + }, + }, + { + "name": "specular", + "review_channels": { + "R": "specular.R", + "G": "specular.G", + "B": "specular.B", + "A": "specular.A", + }, + }, + ]) From 591767152131c6be76db170f10a745a65e79fd5d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:04:01 +0200 Subject: [PATCH 11/95] Add AR, AG, AB test case and fix behavior --- client/ayon_core/lib/transcoding.py | 2 +- .../client/ayon_core/lib/test_transcoding.py | 23 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 57ae6cb0bc..895e220729 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -389,7 +389,7 @@ def get_review_info_by_layer_name(channel_names): layer_names_order.append(layer_name) # R, G, B, A or X, Y, Z, N, AR, AG, AB - channel = last_part[0].upper() + channel = last_part.upper() channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list diff --git a/tests/client/ayon_core/lib/test_transcoding.py b/tests/client/ayon_core/lib/test_transcoding.py index f637e8bd9e..b9959e2958 100644 --- a/tests/client/ayon_core/lib/test_transcoding.py +++ b/tests/client/ayon_core/lib/test_transcoding.py @@ -82,6 +82,29 @@ class GetReviewInfoByLayerName(unittest.TestCase): } }]) + def test_ar_ag_ab_channels(self): + + info = get_review_info_by_layer_name(["AR", "AG", "AB"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "AR", + "G": "AG", + "B": "AB", + "A": None, + } + }]) + + info = get_review_info_by_layer_name(["AR", "AG", "AB", "A"]) + self.assertEqual(info, [{ + "name": "", + "review_channels": { + "R": "AR", + "G": "AG", + "B": "AB", + "A": "A", + } + }]) def test_unknown_channels(self): info = get_review_info_by_layer_name(["hello", "world"]) From 00921e7806ab345037030fc8bb5ebd426840ce53 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 19 May 2025 12:05:28 +0200 Subject: [PATCH 12/95] Update client/ayon_core/lib/transcoding.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 895e220729..a22b8aaf64 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -74,7 +74,6 @@ class MissingRGBAChannelsError(ValueError): This may be other channels than solely RGBA, like Z-channel. The error is raised when no matching 'reviewable' channel was found. """ - pass def get_transcode_temp_directory(): From 0c23ecc70d1dd2c2ea8e39c7f752b2cc406f379a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:52:08 +0200 Subject: [PATCH 13/95] Add support for red, green, blue and alpha --- client/ayon_core/lib/transcoding.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index a22b8aaf64..fc0d6bf8cd 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -377,6 +377,8 @@ def get_review_info_by_layer_name(channel_names): if last_part.lower() not in { # Detect RGBA channels "r", "g", "b", "a", + # Support fully written out rgba channel names + "red", "green", "blue", "alpha", # Allow detecting of x, y and z channels, and normal channels "x", "y", "z", "n", # red, green and blue alpha/opacity, for colored mattes @@ -410,10 +412,20 @@ def get_review_info_by_layer_name(channel_names): for layer_name in layer_names_order: channel_info = channels_by_layer_name[layer_name] + alpha = channel_info.get("A") + # RGB channels if all(channel in channel_info for channel in "RGB"): rgb = "R", "G", "B" + # RGB channels using fully written out channel names + elif all( + channel in channel_info + for channel in ("RED", "GREEN", "BLUE") + ): + rgb = "RED", "GREEN", "BLUE" + alpha = channel_info.get("ALPHA") + # XYZ channels (position pass) elif all(channel in channel_info for channel in "XYZ"): rgb = "X", "Y", "Z" @@ -443,7 +455,7 @@ def get_review_info_by_layer_name(channel_names): "R": red, "G": green, "B": blue, - "A": channel_info.get("A"), + "A": alpha, } }) return output From 55bfd79cf3cdfd4225b5e7ffdb9b660e3dd23592 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:55:21 +0200 Subject: [PATCH 14/95] Check against `.upper()` instead of `.lower()` to match strings more with how they are compared later in the code (improve style consistency) --- client/ayon_core/lib/transcoding.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index fc0d6bf8cd..643a056563 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -374,23 +374,23 @@ def get_review_info_by_layer_name(channel_names): if "." in channel_name: layer_name, last_part = channel_name.rsplit(".", 1) - if last_part.lower() not in { + # R, G, B, A or X, Y, Z, N, AR, AG, AB, RED, GREEN, BLUE, ALPHA + channel = last_part.upper() + if channel not in { # Detect RGBA channels - "r", "g", "b", "a", + "R", "G", "B", "A", # Support fully written out rgba channel names - "red", "green", "blue", "alpha", + "RED", "GREEN", "BLUE", "ALPHA", # Allow detecting of x, y and z channels, and normal channels - "x", "y", "z", "n", + "X", "Y", "Z", "N", # red, green and blue alpha/opacity, for colored mattes - "ar", "ag", "ab" + "AR", "AG", "AB" }: continue if layer_name not in layer_names_order: layer_names_order.append(layer_name) - # R, G, B, A or X, Y, Z, N, AR, AG, AB - channel = last_part.upper() channels_by_layer_name[layer_name][channel] = channel_name # Put empty layer to the beginning of the list From 4a1755c7c58f60706f048e628e6005017406aaef Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 17 Oct 2025 16:06:37 +0200 Subject: [PATCH 15/95] Fixes source metadata key in OTIO review extraction The change corrects the metadata key prefixes used when extracting source width and height information for OTIO reviewable representations. It removes the trailing period from the prefixes "ayon.source." and "openpype.source." to ensure accurate retrieval of resolution data. This resolves an issue where incorrect or missing resolution information could lead to squished reviewables. --- client/ayon_core/plugins/publish/extract_otio_review.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_otio_review.py b/client/ayon_core/plugins/publish/extract_otio_review.py index 90215bd2c9..f338fba746 100644 --- a/client/ayon_core/plugins/publish/extract_otio_review.py +++ b/client/ayon_core/plugins/publish/extract_otio_review.py @@ -130,7 +130,7 @@ class ExtractOTIOReview( # NOTE it looks like it is set only in hiero integration res_data = {"width": self.to_width, "height": self.to_height} for key in res_data: - for meta_prefix in ("ayon.source.", "openpype.source."): + for meta_prefix in ("ayon.source", "openpype.source"): meta_key = f"{meta_prefix}.{key}" value = media_metadata.get(meta_key) if value is not None: From 0dd5620de61614e90efe0632bc54cc0e99089347 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 16:27:23 +0200 Subject: [PATCH 16/95] better typehints --- client/ayon_core/lib/path_templates.py | 93 +++++++++++++------------- 1 file changed, 48 insertions(+), 45 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index c6e9e14eac..ccbea01fa6 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import re import copy @@ -5,11 +7,7 @@ import numbers import warnings import platform from string import Formatter -import typing -from typing import List, Dict, Any, Set - -if typing.TYPE_CHECKING: - from typing import Union +from typing import Any, Union SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") @@ -84,7 +82,7 @@ class StringTemplate: if substr: new_parts.append(substr) - self._parts: List["Union[str, OptionalPart, FormattingPart]"] = ( + self._parts: list[Union[str, OptionalPart, FormattingPart]] = ( self.find_optional_parts(new_parts) ) @@ -105,7 +103,7 @@ class StringTemplate: def template(self) -> str: return self._template - def format(self, data: Dict[str, Any]) -> "TemplateResult": + def format(self, data: dict[str, Any]) -> "TemplateResult": """ Figure out with whole formatting. Separate advanced keys (*Like '{project[name]}') from string which must @@ -145,29 +143,29 @@ class StringTemplate: invalid_types ) - def format_strict(self, data: Dict[str, Any]) -> "TemplateResult": + def format_strict(self, data: dict[str, Any]) -> "TemplateResult": result = self.format(data) result.validate() return result @classmethod def format_template( - cls, template: str, data: Dict[str, Any] + cls, template: str, data: dict[str, Any] ) -> "TemplateResult": objected_template = cls(template) return objected_template.format(data) @classmethod def format_strict_template( - cls, template: str, data: Dict[str, Any] + cls, template: str, data: dict[str, Any] ) -> "TemplateResult": objected_template = cls(template) return objected_template.format_strict(data) @staticmethod def find_optional_parts( - parts: List["Union[str, FormattingPart]"] - ) -> List["Union[str, OptionalPart, FormattingPart]"]: + parts: list[Union[str, FormattingPart]] + ) -> list[Union[str, OptionalPart, FormattingPart]]: new_parts = [] tmp_parts = {} counted_symb = -1 @@ -192,7 +190,7 @@ class StringTemplate: len(parts) == 1 and isinstance(parts[0], str) ): - value = "<{}>".format(parts[0]) + value = f"<{parts[0]}>" else: value = OptionalPart(parts) @@ -223,7 +221,7 @@ class TemplateResult(str): only used keys. solved (bool): For check if all required keys were filled. template (str): Original template. - missing_keys (Iterable[str]): Missing keys that were not in the data. + missing_keys (list[str]): Missing keys that were not in the data. Include missing optional keys. invalid_types (dict): When key was found in data, but value had not allowed DataType. Allowed data types are `numbers`, @@ -232,11 +230,11 @@ class TemplateResult(str): of number. """ - used_values: Dict[str, Any] = None + used_values: dict[str, Any] = None solved: bool = None template: str = None - missing_keys: List[str] = None - invalid_types: Dict[str, Any] = None + missing_keys: list[str] = None + invalid_types: dict[str, Any] = None def __new__( cls, filled_template, template, solved, @@ -296,21 +294,21 @@ class TemplatePartResult: """Result to store result of template parts.""" def __init__(self, optional: bool = False): # Missing keys or invalid value types of required keys - self._missing_keys: Set[str] = set() - self._invalid_types: Dict[str, Any] = {} + self._missing_keys: set[str] = set() + self._invalid_types: dict[str, Any] = {} # Missing keys or invalid value types of optional keys - self._missing_optional_keys: Set[str] = set() - self._invalid_optional_types: Dict[str, Any] = {} + self._missing_optional_keys: set[str] = set() + self._invalid_optional_types: dict[str, Any] = {} # Used values stored by key with origin type # - key without any padding or key modifiers # - value from filling data # Example: {"version": 1} - self._used_values: Dict[str, Any] = {} + self._used_values: dict[str, Any] = {} # Used values stored by key with all modifirs # - value is already formatted string # Example: {"version:0>3": "001"} - self._really_used_values: Dict[str, Any] = {} + self._really_used_values: dict[str, Any] = {} # Concatenated string output after formatting self._output: str = "" # Is this result from optional part @@ -336,8 +334,9 @@ class TemplatePartResult: self._really_used_values.update(other.really_used_values) else: - raise TypeError("Cannot add data from \"{}\" to \"{}\"".format( - str(type(other)), self.__class__.__name__) + raise TypeError( + f"Cannot add data from \"{type(other)}\"" + f" to \"{self.__class__.__name__}\"" ) @property @@ -362,40 +361,41 @@ class TemplatePartResult: return self._output @property - def missing_keys(self) -> Set[str]: + def missing_keys(self) -> set[str]: return self._missing_keys @property - def missing_optional_keys(self) -> Set[str]: + def missing_optional_keys(self) -> set[str]: return self._missing_optional_keys @property - def invalid_types(self) -> Dict[str, Any]: + def invalid_types(self) -> dict[str, Any]: return self._invalid_types @property - def invalid_optional_types(self) -> Dict[str, Any]: + def invalid_optional_types(self) -> dict[str, Any]: return self._invalid_optional_types @property - def really_used_values(self) -> Dict[str, Any]: + def really_used_values(self) -> dict[str, Any]: return self._really_used_values @property - def realy_used_values(self) -> Dict[str, Any]: + def realy_used_values(self) -> dict[str, Any]: warnings.warn( "Property 'realy_used_values' is deprecated." " Use 'really_used_values' instead.", - DeprecationWarning + DeprecationWarning, + stacklevel=2, ) return self._really_used_values @property - def used_values(self) -> Dict[str, Any]: + def used_values(self) -> dict[str, Any]: return self._used_values @staticmethod - def split_keys_to_subdicts(values: Dict[str, Any]) -> Dict[str, Any]: + def split_keys_to_subdicts(values: dict[str, Any]) -> dict[str, Any]: output = {} formatter = Formatter() for key, value in values.items(): @@ -410,7 +410,7 @@ class TemplatePartResult: data[last_key] = value return output - def get_clean_used_values(self) -> Dict[str, Any]: + def get_clean_used_values(self) -> dict[str, Any]: new_used_values = {} for key, value in self.used_values.items(): if isinstance(value, FormatObject): @@ -426,7 +426,8 @@ class TemplatePartResult: warnings.warn( "Method 'add_realy_used_value' is deprecated." " Use 'add_really_used_value' instead.", - DeprecationWarning + DeprecationWarning, + stacklevel=2, ) self.add_really_used_value(key, value) @@ -479,7 +480,7 @@ class FormattingPart: self, field_name: str, format_spec: str, - conversion: "Union[str, None]", + conversion: Union[str, None], ): format_spec_v = "" if format_spec: @@ -546,7 +547,7 @@ class FormattingPart: return not queue @staticmethod - def keys_to_template_base(keys: List[str]): + def keys_to_template_base(keys: list[str]): if not keys: return None # Create copy of keys @@ -556,7 +557,7 @@ class FormattingPart: return f"{template_base}{joined_keys}" def format( - self, data: Dict[str, Any], result: TemplatePartResult + self, data: dict[str, Any], result: TemplatePartResult ) -> TemplatePartResult: """Format the formattings string. @@ -687,23 +688,25 @@ class OptionalPart: def __init__( self, - parts: List["Union[str, OptionalPart, FormattingPart]"] + parts: list[Union[str, OptionalPart, FormattingPart]] ): - self._parts: List["Union[str, OptionalPart, FormattingPart]"] = parts + self._parts: list[Union[str, OptionalPart, FormattingPart]] = parts @property - def parts(self) -> List["Union[str, OptionalPart, FormattingPart]"]: + def parts(self) -> list[Union[str, OptionalPart, FormattingPart]]: return self._parts def __str__(self) -> str: - return "<{}>".format("".join([str(p) for p in self._parts])) + joined_parts = "".join([str(p) for p in self._parts]) + return f"<{joined_parts}>" def __repr__(self) -> str: - return "".format("".join([str(p) for p in self._parts])) + joined_parts = "".join([str(p) for p in self._parts]) + return f"" def format( self, - data: Dict[str, Any], + data: dict[str, Any], result: TemplatePartResult, ) -> TemplatePartResult: new_result = TemplatePartResult(True) From 4fca5bcde5f086cc37ea3a961cd0b872276fb438 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 17:04:56 +0200 Subject: [PATCH 17/95] Implemented helper dict to handle str -> dict conversion --- client/ayon_core/lib/path_templates.py | 68 +++++++++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index ccbea01fa6..131a2efaa4 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -7,7 +7,7 @@ import numbers import warnings import platform from string import Formatter -from typing import Any, Union +from typing import Any, Union, Iterable SUB_DICT_PATTERN = re.compile(r"([^\[\]]+)") OPTIONAL_PATTERN = re.compile(r"(<.*?[^{0]*>)[^0-9]*?") @@ -42,6 +42,66 @@ class TemplateUnsolved(Exception): ) +class DefaultValueDict(dict): + """Dictionary that supports the default key to use for str conversion. + + Is helpful for changes of a key in a template from string to dictionary + for example '{folder}' -> '{folder[name]}'. + >>> data = DefaultValueDict( + >>> "name", + >>> {"folder": {"name": "FolderName"}} + >>> ) + >>> print("{folder[name]}".format_map(data)) + FolderName + >>> print("{folder}".format_map(data)) + FolderName + + Args: + default_key (Union[str, Iterable[str]]): Default key to use for str + conversion. Can also expect multiple keys for more nested + dictionary. + + """ + def __init__( + self, default_keys: Union[str, Iterable[str]], *args, **kwargs + ) -> None: + if isinstance(default_keys, str): + default_keys = [default_keys] + else: + default_keys = list(default_keys) + if not default_keys: + raise ValueError( + "Default key must be set. Got empty default keys." + ) + + self._default_keys = default_keys + super().__init__(*args, **kwargs) + + def __str__(self) -> str: + return str(self.get_default_value()) + + def __copy__(self) -> "DefaultValueDict": + return DefaultValueDict( + self.get_default_keys(), dict(self.items()) + ) + + def __deepcopy__(self) -> "DefaultValueDict": + data_copy = { + key: copy.deepcopy(value) + for key, value in self.items() + } + return DefaultValueDict(self.get_default_keys(), data_copy) + + def get_default_keys(self) -> list[str]: + return list(self._default_keys) + + def get_default_value(self) -> Any: + value = self + for key in self._default_keys: + value = value[key] + return value + + class StringTemplate: """String that can be formatted.""" def __init__(self, template: str): @@ -636,6 +696,12 @@ class FormattingPart: result.add_output(self.template) return result + if isinstance(value, DefaultValueDict): + try: + value = value.get_default_value() + except KeyError: + pass + if not self.validate_value_type(value): result.add_invalid_type(key, value) result.add_output(self.template) From f35521a943b14ae0d38e2b2bb005b7ace2fc25cd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:00:00 +0200 Subject: [PATCH 18/95] rename 'DefaultValueDict' to 'DefaultKeysDict' --- client/ayon_core/lib/__init__.py | 2 ++ client/ayon_core/lib/path_templates.py | 14 +++++++------- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 5ccc8d03e5..2a25e949a5 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -73,6 +73,7 @@ from .log import ( ) from .path_templates import ( + DefaultKeysDict, TemplateUnsolved, StringTemplate, FormatObject, @@ -228,6 +229,7 @@ __all__ = [ "get_version_from_path", "get_last_version_from_path", + "DefaultKeysDict", "TemplateUnsolved", "StringTemplate", "FormatObject", diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index 131a2efaa4..c01de6f1a6 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -42,12 +42,12 @@ class TemplateUnsolved(Exception): ) -class DefaultValueDict(dict): +class DefaultKeysDict(dict): """Dictionary that supports the default key to use for str conversion. Is helpful for changes of a key in a template from string to dictionary for example '{folder}' -> '{folder[name]}'. - >>> data = DefaultValueDict( + >>> data = DefaultKeysDict( >>> "name", >>> {"folder": {"name": "FolderName"}} >>> ) @@ -80,17 +80,17 @@ class DefaultValueDict(dict): def __str__(self) -> str: return str(self.get_default_value()) - def __copy__(self) -> "DefaultValueDict": - return DefaultValueDict( + def __copy__(self) -> "DefaultKeysDict": + return DefaultKeysDict( self.get_default_keys(), dict(self.items()) ) - def __deepcopy__(self) -> "DefaultValueDict": + def __deepcopy__(self) -> "DefaultKeysDict": data_copy = { key: copy.deepcopy(value) for key, value in self.items() } - return DefaultValueDict(self.get_default_keys(), data_copy) + return DefaultKeysDict(self.get_default_keys(), data_copy) def get_default_keys(self) -> list[str]: return list(self._default_keys) @@ -696,7 +696,7 @@ class FormattingPart: result.add_output(self.template) return result - if isinstance(value, DefaultValueDict): + if isinstance(value, DefaultKeysDict): try: value = value.get_default_value() except KeyError: From a798d9b92ba957959b483c3ed28c116e9d2df22c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:01:40 +0200 Subject: [PATCH 19/95] added helper function to get ayon user data --- client/ayon_core/lib/__init__.py | 2 + client/ayon_core/lib/local_settings.py | 73 ++++++++++++++++++++------ 2 files changed, 58 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index 2a25e949a5..d5629cbf3d 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -11,6 +11,7 @@ from .local_settings import ( get_launcher_storage_dir, get_addons_resources_dir, get_local_site_id, + get_ayon_user_entity, get_ayon_username, ) from .ayon_connection import initialize_ayon_connection @@ -149,6 +150,7 @@ __all__ = [ "get_launcher_storage_dir", "get_addons_resources_dir", "get_local_site_id", + "get_ayon_user_entity", "get_ayon_username", "initialize_ayon_connection", diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 1edfc3c1b6..4402e3c8a1 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -5,6 +5,7 @@ import json import platform import configparser import warnings +import copy from datetime import datetime from abc import ABC, abstractmethod from functools import lru_cache @@ -13,6 +14,8 @@ from typing import Optional, Any import platformdirs import ayon_api +from .cache import NestedCacheItem, CacheItem + _PLACEHOLDER = object() @@ -23,6 +26,7 @@ class RegistryItemNotFound(ValueError): class _Cache: username = None + user_entities_by_name = NestedCacheItem() def _get_ayon_appdirs(*args: str) -> str: @@ -569,6 +573,56 @@ def get_local_site_id(): return site_id +def _get_ayon_service_username() -> Optional[str]: + # TODO @iLLiCiTiT - do not use private attribute of 'ServerAPI', rather + # use public method to get username from connection stack. + con = ayon_api.get_server_api_connection() + user_stack = getattr(con, "_as_user_stack", None) + if user_stack is None: + return None + return user_stack.username + + +def get_ayon_user_entity(username: Optional[str] = None) -> dict[str, Any]: + """AYON user entity used for templates and publishing.""" + service_username = _get_ayon_service_username() + # Handle service user handling first + if service_username: + if username is None: + username = service_username + cache: CacheItem = _Cache.user_entities_by_name[username] + if not cache.is_valid: + if username == service_username: + user = ayon_api.get_user() + else: + user = ayon_api.get_user(username) + cache.update_data(user) + return copy.deepcopy(cache.get_data()) + + # Cache current user + current_user = None + if _Cache.username is None: + current_user = ayon_api.get_user() + _Cache.username = current_user["name"] + + if username is None: + username = _Cache.username + + cache: CacheItem = _Cache.user_entities_by_name[username] + if not cache.is_valid: + user = None + if username == _Cache.username: + if current_user is None: + current_user = ayon_api.get_user() + user = current_user + + if user is None: + user = ayon_api.get_user(username) + cache.update_data(user) + + return copy.deepcopy(cache.get_data()) + + def get_ayon_username(): """AYON username used for templates and publishing. @@ -578,20 +632,5 @@ def get_ayon_username(): str: Username. """ - # Look for username in the connection stack - # - this is used when service is working as other user - # (e.g. in background sync) - # TODO @iLLiCiTiT - do not use private attribute of 'ServerAPI', rather - # use public method to get username from connection stack. - con = ayon_api.get_server_api_connection() - user_stack = getattr(con, "_as_user_stack", None) - if user_stack is not None: - username = user_stack.username - if username is not None: - return username - - # Cache the username to avoid multiple API calls - # - it is not expected that user would change - if _Cache.username is None: - _Cache.username = ayon_api.get_user()["name"] - return _Cache.username + user = get_ayon_user_entity() + return user["name"] From d7f913d00478bc5b29f0e54c32ac0a548e9aa870 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 20 Oct 2025 18:03:46 +0200 Subject: [PATCH 20/95] fill user data as dictionary --- client/ayon_core/pipeline/template_data.py | 41 +++++++++++++++++----- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index 0a95a98be8..2f9a7e3421 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -1,27 +1,50 @@ +from __future__ import annotations + +from typing import Optional, Any + import ayon_api from ayon_core.settings import get_studio_settings -from ayon_core.lib.local_settings import get_ayon_username +from ayon_core.lib import DefaultKeysDict +from ayon_core.lib.local_settings import get_ayon_user_entity -def get_general_template_data(settings=None, username=None): +def get_general_template_data( + settings: Optional[dict[str, Any]] = None, + username: Optional[str] = None, + user_entity: Optional[dict[str, Any]] = None, +): """General template data based on system settings or machine. Output contains formatting keys: - - 'studio[name]' - Studio name filled from system settings - - 'studio[code]' - Studio code filled from system settings - - 'user' - User's name using 'get_ayon_username' + - 'studio[name]' - Studio name filled from system settings + - 'studio[code]' - Studio code filled from system settings + - 'user[name]' - User's name + - 'user[attrib][...]' - User's attributes + - 'user[data][...]' - User's data Args: settings (Dict[str, Any]): Studio or project settings. username (Optional[str]): AYON Username. - """ + user_entity (Optional[dict[str, Any]]): User entity. + """ if not settings: settings = get_studio_settings() - if username is None: - username = get_ayon_username() + if user_entity is None: + user_entity = get_ayon_user_entity(username) + + # Use dictionary with default value for backwards compatibility + # - we did support '{user}' now it should be '{user[name]}' + user_data = DefaultKeysDict( + "name", + { + "name": user_entity["name"], + "attrib": user_entity["attrib"], + "data": user_entity["data"], + } + ) core_settings = settings["core"] return { @@ -29,7 +52,7 @@ def get_general_template_data(settings=None, username=None): "name": core_settings["studio_name"], "code": core_settings["studio_code"] }, - "user": username + "user": user_data, } From 0d49f5a8dfda7b2b3b5189befbdf4ac34388c5e4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Oct 2025 11:14:10 +0200 Subject: [PATCH 21/95] Fixes: Corrects file sequence frame offset Corrects the calculation of the frame offset for file sequences in editorial workflows. - Ensures accurate frame mapping. - Resolves issues with incorrect frame ranges. --- client/ayon_core/pipeline/editorial.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index b553fae3fb..716035aa1c 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -202,7 +202,8 @@ def is_clip_from_media_sequence(otio_clip): def remap_range_on_file_sequence(otio_clip, otio_range): - """ + """ Remap the provided range on a file sequence clip. + Args: otio_clip (otio.schema.Clip): The OTIO clip to check. otio_range (otio.schema.TimeRange): The trim range to apply. @@ -256,10 +257,14 @@ def remap_range_on_file_sequence(otio_clip, otio_range): ) src_offset_in = otio_range.start_time - media_in - frame_in = otio.opentime.RationalTime.from_frames( - media_ref.start_frame + src_offset_in.to_frames(), + # make sure that only if any offset is present + if media_ref.start_frame == src_offset_in.to_frames(): + frame_in = src_offset_in.to_frames() + else: + frame_in = otio.opentime.RationalTime.from_frames( + media_ref.start_frame + src_offset_in.to_frames(), rate=available_range_rate, - ).to_frames() + ).to_frames() # e.g.: # duration = 10 frames at 24fps From 9494472a7dbc1726b3fbe1face11feff2c8b4c9e Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Tue, 21 Oct 2025 14:18:31 +0200 Subject: [PATCH 22/95] Fixes: Corrects reviewable output resolution. Updates the expected resolution in the ffmpeg commands used in reviewable extraction tests to match the intended output. This resolves a squashed reviewables issue where the output resolution was incorrect. --- .../editorial/test_extract_otio_review.py | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py index 6a74df7f43..ed441edc63 100644 --- a/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py +++ b/tests/client/ayon_core/pipeline/editorial/test_extract_otio_review.py @@ -246,75 +246,75 @@ def test_multiple_review_clips_no_gap(): expected = [ # 10 head black frames generated from gap (991-1000) '/path/to/ffmpeg -t 0.4 -r 25.0 -f lavfi' - ' -i color=c=black:s=1280x720 -tune ' + ' -i color=c=black:s=1920x1080 -tune ' 'stillimage -start_number 991 -pix_fmt rgba C:/result/output.%04d.png', # Alternance 25fps tiff sequence and 24fps exr sequence # for 100 frames each '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1001 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1102 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1198 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1299 -pix_fmt rgba C:/result/output.%04d.png', # Repeated 25fps tiff sequence multiple times till the end '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1395 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1496 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1597 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1698 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1799 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1900 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 2001 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 2102 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 25.0 -i ' f'C:\\no_tc{os.sep}output.%04d.tif ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 2203 -pix_fmt rgba C:/result/output.%04d.png' ] @@ -348,12 +348,12 @@ def test_multiple_review_clips_with_gap(): '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1003 -pix_fmt rgba C:/result/output.%04d.png', '/path/to/ffmpeg -start_number 1000 -framerate 24.0 -i ' f'C:\\with_tc{os.sep}output.%04d.exr ' - '-vf scale=1280:720:flags=lanczos -compression_level 5 ' + '-vf scale=1920:1080:flags=lanczos -compression_level 5 ' '-start_number 1091 -pix_fmt rgba C:/result/output.%04d.png' ] From 54aedc84263fb2adc838cbaf40e59bce6aa84a24 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:31:34 +0200 Subject: [PATCH 23/95] use user entity when getting template data during publishing --- .../plugins/publish/collect_anatomy_context_data.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_context_data.py b/client/ayon_core/plugins/publish/collect_anatomy_context_data.py index cccf392e40..5d2ecec433 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_context_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_context_data.py @@ -16,6 +16,7 @@ Provides: import json import pyblish.api +from ayon_core.lib import get_ayon_user_entity from ayon_core.pipeline.template_data import get_template_data @@ -55,17 +56,18 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): if folder_entity: task_entity = context.data["taskEntity"] + username = context.data["user"] + user_entity = get_ayon_user_entity(username) anatomy_data = get_template_data( project_entity, folder_entity, task_entity, - host_name, - project_settings + host_name=host_name, + settings=project_settings, + user_entity=user_entity, ) anatomy_data.update(context.data.get("datetimeData") or {}) - username = context.data["user"] - anatomy_data["user"] = username # Backwards compatibility for 'username' key anatomy_data["username"] = username From 2e2d67c2438b53f12354f32b46c2cfb57ecf5ab8 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:31:57 +0200 Subject: [PATCH 24/95] allow to pass user entity to get_template_data --- client/ayon_core/pipeline/template_data.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index 2f9a7e3421..dc7e95c788 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -173,7 +173,8 @@ def get_template_data( task_entity=None, host_name=None, settings=None, - username=None + username=None, + user_entity=None, ): """Prepare data for templates filling from entered documents and info. @@ -196,13 +197,18 @@ def get_template_data( host_name (Optional[str]): Used to fill '{app}' key. settings (Union[Dict, None]): Prepared studio or project settings. They're queried if not passed (may be slower). - username (Optional[str]): AYON Username. + username (Optional[str]): DEPRECATED AYON Username. + user_entity (Optional[dict[str, Any]): AYON user entity. Returns: Dict[str, Any]: Data prepared for filling workdir template. """ - template_data = get_general_template_data(settings, username=username) + template_data = get_general_template_data( + settings, + username=username, + user_entity=user_entity, + ) template_data.update(get_project_template_data(project_entity)) if folder_entity: template_data.update(get_folder_template_data( From d700f9f09b307243ef7c30ad5096e3beb8b97d9b Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:32:12 +0200 Subject: [PATCH 25/95] custom handling of 'user' data used for template --- client/ayon_core/plugins/publish/integrate.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index f1e066018c..d18e546392 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -121,7 +121,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "version", "representation", "username", - "user", "output", # OpenPype keys - should be removed "asset", # folder[name] @@ -796,6 +795,14 @@ class IntegrateAsset(pyblish.api.InstancePlugin): if value is not None: repre_context[key] = value + # Keep only username + # NOTE This is to avoid storing all user attributes and data + # to representation + if "user" not in repre_context: + repre_context["user"] = { + "name": template_data["user"]["name"] + } + # Use previous representation's id if there is a name match existing = existing_repres_by_name.get(repre["name"].lower()) repre_id = None From b094cbd0cb17ba0216867503c9d87eef81e112f3 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:33:48 +0200 Subject: [PATCH 26/95] use same logic in integrate hero as in integrate --- .../ayon_core/plugins/publish/integrate_hero_version.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index 90e6f15568..a591cfe880 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -89,7 +89,6 @@ class IntegrateHeroVersion( "family", "representation", "username", - "user", "output" ] # QUESTION/TODO this process should happen on server if crashed due to @@ -364,6 +363,14 @@ class IntegrateHeroVersion( if value is not None: repre_context[key] = value + # Keep only username + # NOTE This is to avoid storing all user attributes and data + # to representation + if "user" not in repre_context: + repre_context["user"] = { + "name": anatomy_data["user"]["name"] + } + # Prepare new repre repre_entity = copy.deepcopy(repre_info["representation"]) repre_entity.pop("id", None) From db11ba743708d9550357f3129b7be9356c09b4c2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 22 Oct 2025 14:59:30 +0200 Subject: [PATCH 27/95] add docstring --- client/ayon_core/lib/local_settings.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 4402e3c8a1..8a17b7af38 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -584,7 +584,19 @@ def _get_ayon_service_username() -> Optional[str]: def get_ayon_user_entity(username: Optional[str] = None) -> dict[str, Any]: - """AYON user entity used for templates and publishing.""" + """AYON user entity used for templates and publishing. + + Note: + Usually only service and admin users can receive the full user entity. + + Args: + username (Optional[str]): Username of the user. If not passed, then + the current user in 'ayon_api' is used. + + Returns: + dict[str, Any]: User entity. + + """ service_username = _get_ayon_service_username() # Handle service user handling first if service_username: From 062f756413ec17332005d59ce7039cedf76cff21 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Oct 2025 16:07:26 +0200 Subject: [PATCH 28/95] Typing At least some, dont know how to import NewFolderDict --- .../tools/push_to_project/models/integrate.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index ef49838152..472500a55d 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -5,7 +5,7 @@ import itertools import sys import traceback import uuid -from typing import Optional, Dict +from typing import Optional, Dict, Any import ayon_api from ayon_api.utils import create_entity_id @@ -650,10 +650,10 @@ class ProjectPushItemProcess: def _create_folder( self, - src_folder_entity, - project_entity, - parent_folder_entity, - folder_name + src_folder_entity: Dict[str, Any], + project_entity: Dict[str, Any], + parent_folder_entity: Dict[str, Any], + folder_name: str ): parent_id = None if parent_folder_entity: From 475d4800a2e86f83ed508e0b33c425a7d6ab9eb7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Oct 2025 16:13:44 +0200 Subject: [PATCH 29/95] Check that source folder type could be pushed to destination --- .../tools/push_to_project/models/integrate.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 472500a55d..bd309d935f 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -702,7 +702,11 @@ class ProjectPushItemProcess: if new_folder_name != folder_name: folder_label = folder_name - # TODO find out how to define folder type + src_folder_type = src_folder_entity["folderType"] + self._check_src_folder_type( + project_entity, + src_folder_type + ) folder_entity = new_folder_entity( folder_name, "Folder", @@ -727,6 +731,24 @@ class ProjectPushItemProcess: folder_entity["path"] = "/".join([parent_path, folder_name]) return folder_entity + def _check_src_folder_type( + self, + project_entity: Dict[str, Any], + src_folder_type: str + ): + """Confirm that folder type exists in destination project""" + folder_types = [ + folder_type["name"].lower() + for folder_type in project_entity["folderTypes"] + ] + + if src_folder_type.lower() not in folder_types: + self._status.set_failed( + f"'{src_folder_type}' folder type is not configured in " + f"project Anatomy." + ) + raise PushToProjectError(self._status.fail_reason) + def _fill_or_create_destination_folder(self): dst_project_name = self._item.dst_project_name dst_folder_id = self._item.dst_folder_id @@ -1205,7 +1227,7 @@ class ProjectPushItemProcess: value_to_update = formatting_data.get(context_key) if value_to_update: repre_context[context_key] = value_to_update - if "task" not in formatting_data: + if "task" not in formatting_data and "task" in repre_context: repre_context.pop("task") return repre_context From 9340df7a250543658d673d95716047931ea981fa Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Oct 2025 16:14:01 +0200 Subject: [PATCH 30/95] Copy source folder type to destination --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index bd309d935f..22fcb5cf9f 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -709,7 +709,7 @@ class ProjectPushItemProcess: ) folder_entity = new_folder_entity( folder_name, - "Folder", + src_folder_type, parent_id=parent_id, attribs=new_folder_attrib ) From a077c57eee066f2c38082ea3d023451bde66a487 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 22 Oct 2025 14:50:25 +0000 Subject: [PATCH 31/95] [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 d3b3454fd1..4aeeb94ea8 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.5+dev" +__version__ = "1.6.6" diff --git a/package.py b/package.py index 2889039502..ced8763100 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.5+dev" +version = "1.6.6" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index f43846ec2b..7460ddc831 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.5+dev" +version = "1.6.6" description = "" authors = ["Ynput Team "] readme = "README.md" From 45ddec53d30a50d169c33cea530fde83cd5a7821 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 22 Oct 2025 14:51:09 +0000 Subject: [PATCH 32/95] [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 4aeeb94ea8..8e0834b8da 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.6" +__version__ = "1.6.6+dev" diff --git a/package.py b/package.py index ced8763100..5fa4d165d2 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.6" +version = "1.6.6+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 7460ddc831..73b9a4a916 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.6" +version = "1.6.6+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 110018487f15f9a49156d1947592bf8541c12bc4 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 22 Oct 2025 14:52:19 +0000 Subject: [PATCH 33/95] 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 646a2dd1ee..60693f088d 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.6 - 1.6.5 - 1.6.4 - 1.6.3 From 0d235ed8cacffb9196a54e1b3cf40fbcc36fd57e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 22 Oct 2025 18:58:20 +0200 Subject: [PATCH 34/95] Create destination task if no task selected --- .../tools/push_to_project/models/integrate.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index ef49838152..a7cb1de95a 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -762,8 +762,11 @@ class ProjectPushItemProcess: ) self._folder_entity = folder_entity if not dst_task_name: - self._task_info = {} - return + dst_task_name = self._make_sure_task_exists(folder_entity) + + if not dst_task_name: # really no task selected nor on source + self._task_info = {} + return folder_path = folder_entity["path"] folder_tasks = { @@ -962,6 +965,28 @@ class ProjectPushItemProcess: ) self._version_entity = version_entity + def _make_sure_task_exists(self, folder_entity: Dict[str, Any]) -> str: + """Creates destination task from source task information""" + project_name = self._item.dst_project_name + src_version_entity = self._src_version_entity + src_task = ayon_api.get_task_by_id( + self._item.src_project_name, src_version_entity["taskId"] + ) + if not src_task: + self._status.set_failed( + f"No task selected and couldn't find source task" + ) + raise PushToProjectError(self._status.fail_reason) + _task_id = ayon_api.create_task( + project_name, + src_task["name"], + folder_id=folder_entity["id"], + task_type=src_task["taskType"], + attrib=src_task["attrib"], + ) + + return src_task["name"] + def _integrate_representations(self): try: self._real_integrate_representations() From 0ebbd0a23224e4fd539f8b1a479892de38af96dc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 10:55:37 +0200 Subject: [PATCH 35/95] Extracted logic to methods --- .../tools/push_to_project/models/integrate.py | 52 +++++++++++++------ 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index a7cb1de95a..9365379148 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -482,6 +482,8 @@ class ProjectPushItemProcess: self._log_info("Destination project was found") self._fill_or_create_destination_folder() self._log_info("Destination folder was determined") + self._fill_or_create_destination_task() + self._log_info("Destination task was determined") self._determine_product_type() self._determine_publish_template_name() self._determine_product_name() @@ -730,7 +732,6 @@ class ProjectPushItemProcess: def _fill_or_create_destination_folder(self): dst_project_name = self._item.dst_project_name dst_folder_id = self._item.dst_folder_id - dst_task_name = self._item.dst_task_name new_folder_name = self._item.new_folder_name if not dst_folder_id and not new_folder_name: self._status.set_failed( @@ -761,12 +762,11 @@ class ProjectPushItemProcess: new_folder_name ) self._folder_entity = folder_entity - if not dst_task_name: - dst_task_name = self._make_sure_task_exists(folder_entity) - if not dst_task_name: # really no task selected nor on source - self._task_info = {} - return + def _fill_or_create_destination_task(self): + folder_entity = self._folder_entity + dst_task_name = self._item.dst_task_name + dst_project_name = self._item.dst_project_name folder_path = folder_entity["path"] folder_tasks = { @@ -775,6 +775,21 @@ class ProjectPushItemProcess: dst_project_name, folder_ids=[folder_entity["id"]] ) } + + if not dst_task_name: + src_task_info = self._get_src_task_info() + if not src_task_info: # really no task selected nor on source + self._task_info = {} + return + + dst_task_name = src_task_info["name"].lower() + if dst_task_name not in folder_tasks: + self._make_sure_task_exists( + folder_entity, src_task_info + ) + task_info = copy.deepcopy(src_task_info) + folder_tasks[dst_task_name] = task_info + task_info = folder_tasks.get(dst_task_name.lower()) if not task_info: self._status.set_failed( @@ -965,9 +980,22 @@ class ProjectPushItemProcess: ) self._version_entity = version_entity - def _make_sure_task_exists(self, folder_entity: Dict[str, Any]) -> str: + def _make_sure_task_exists( + self, + folder_entity: Dict[str, Any], + task_info: Dict[str, Any], + ): """Creates destination task from source task information""" project_name = self._item.dst_project_name + _task_id = ayon_api.create_task( + project_name, + task_info["name"], + folder_id=folder_entity["id"], + task_type=task_info["taskType"], + attrib=task_info["attrib"], + ) + + def _get_src_task_info(self): src_version_entity = self._src_version_entity src_task = ayon_api.get_task_by_id( self._item.src_project_name, src_version_entity["taskId"] @@ -977,15 +1005,7 @@ class ProjectPushItemProcess: f"No task selected and couldn't find source task" ) raise PushToProjectError(self._status.fail_reason) - _task_id = ayon_api.create_task( - project_name, - src_task["name"], - folder_id=folder_entity["id"], - task_type=src_task["taskType"], - attrib=src_task["attrib"], - ) - - return src_task["name"] + return src_task def _integrate_representations(self): try: From d8dd2a23a895b06a45ec27a1c24842729aaa7189 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 10:55:48 +0200 Subject: [PATCH 36/95] Typing --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 9365379148..b2475ac7d1 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -5,7 +5,7 @@ import itertools import sys import traceback import uuid -from typing import Optional, Dict +from typing import Optional, Dict, Any import ayon_api from ayon_api.utils import create_entity_id From ef0f5ac023ebf463567d52074ba10f33caa5936c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Thu, 23 Oct 2025 11:06:35 +0200 Subject: [PATCH 37/95] remove custom copy and deepcopy implementation --- client/ayon_core/lib/path_templates.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/client/ayon_core/lib/path_templates.py b/client/ayon_core/lib/path_templates.py index c01de6f1a6..aba2f296e3 100644 --- a/client/ayon_core/lib/path_templates.py +++ b/client/ayon_core/lib/path_templates.py @@ -80,18 +80,6 @@ class DefaultKeysDict(dict): def __str__(self) -> str: return str(self.get_default_value()) - def __copy__(self) -> "DefaultKeysDict": - return DefaultKeysDict( - self.get_default_keys(), dict(self.items()) - ) - - def __deepcopy__(self) -> "DefaultKeysDict": - data_copy = { - key: copy.deepcopy(value) - for key, value in self.items() - } - return DefaultKeysDict(self.get_default_keys(), data_copy) - def get_default_keys(self) -> list[str]: return list(self._default_keys) From f0230e24a7bc3a2e321caeb14bf7e33acd523eab Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 11:20:03 +0200 Subject: [PATCH 38/95] Fix use operations instead of ayon_api Must be in same session as create folder if 'Create New Folder' --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index b2475ac7d1..027416922b 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -987,7 +987,7 @@ class ProjectPushItemProcess: ): """Creates destination task from source task information""" project_name = self._item.dst_project_name - _task_id = ayon_api.create_task( + _task_id = self._operations.create_task( project_name, task_info["name"], folder_id=folder_entity["id"], From f13a40aa73b740e4cc715cd54acadf564fdb750a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 11:24:41 +0200 Subject: [PATCH 39/95] Fix typing Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 22fcb5cf9f..6871936e2c 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -650,9 +650,9 @@ class ProjectPushItemProcess: def _create_folder( self, - src_folder_entity: Dict[str, Any], - project_entity: Dict[str, Any], - parent_folder_entity: Dict[str, Any], + src_folder_entity: dict[str, Any], + project_entity: dict[str, Any], + parent_folder_entity: dict[str, Any], folder_name: str ): parent_id = None From 87f1d458b8c6cab2195583e0e3beeef8b80e6db1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 11:25:44 +0200 Subject: [PATCH 40/95] Change return of _check_src_folder_type Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../tools/push_to_project/models/integrate.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 6871936e2c..bf14034673 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -731,23 +731,21 @@ class ProjectPushItemProcess: folder_entity["path"] = "/".join([parent_path, folder_name]) return folder_entity - def _check_src_folder_type( + def _get_dst_folder_type( self, - project_entity: Dict[str, Any], + project_entity: dict[str, Any], src_folder_type: str - ): - """Confirm that folder type exists in destination project""" - folder_types = [ - folder_type["name"].lower() - for folder_type in project_entity["folderTypes"] - ] + ) -> str: + """Get new folder type.""" + for folder_type in project_entity["folderTypes"]: + if folder_type["name"].lower() == src_folder_type.lower(): + return folder_type["name"] - if src_folder_type.lower() not in folder_types: - self._status.set_failed( - f"'{src_folder_type}' folder type is not configured in " - f"project Anatomy." - ) - raise PushToProjectError(self._status.fail_reason) + self._status.set_failed( + f"'{src_folder_type}' folder type is not configured in " + f"project Anatomy." + ) + raise PushToProjectError(self._status.fail_reason) def _fill_or_create_destination_folder(self): dst_project_name = self._item.dst_project_name From c50406a279b61651ef6f099862dfe6874d7dcb10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 11:26:09 +0200 Subject: [PATCH 41/95] Simplify pop Co-authored-by: Roy Nieterau --- client/ayon_core/tools/push_to_project/models/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index bf14034673..48e5763345 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1225,8 +1225,8 @@ class ProjectPushItemProcess: value_to_update = formatting_data.get(context_key) if value_to_update: repre_context[context_key] = value_to_update - if "task" not in formatting_data and "task" in repre_context: - repre_context.pop("task") + if "task" not in formatting_data: + repre_context.pop("task", None) return repre_context From 0bade2d940ced81d27d70240b153a813abf8a6c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 11:28:33 +0200 Subject: [PATCH 42/95] Update usage of renamed _get_dst_folder_type --- client/ayon_core/tools/push_to_project/models/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 48e5763345..68a0e2affb 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -703,13 +703,13 @@ class ProjectPushItemProcess: folder_label = folder_name src_folder_type = src_folder_entity["folderType"] - self._check_src_folder_type( + dst_folder_type = self._get_dst_folder_type( project_entity, src_folder_type ) folder_entity = new_folder_entity( folder_name, - src_folder_type, + dst_folder_type, parent_id=parent_id, attribs=new_folder_attrib ) From 1ee701b52fdaafcabe9ce6a726f4d74e9a8a9da5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 11:32:30 +0200 Subject: [PATCH 43/95] Fix dict typing --- .../tools/push_to_project/models/integrate.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 027416922b..1ecf8a8a59 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -5,7 +5,7 @@ import itertools import sys import traceback import uuid -from typing import Optional, Dict, Any +from typing import Optional, Any import ayon_api from ayon_api.utils import create_entity_id @@ -225,8 +225,8 @@ class ProjectPushRepreItem: but filenames are not template based. Args: - repre_entity (Dict[str, Ant]): Representation entity. - roots (Dict[str, str]): Project roots (based on project anatomy). + repre_entity (dict[str, Ant]): Representation entity. + roots (dict[str, str]): Project roots (based on project anatomy). """ def __init__(self, repre_entity, roots): @@ -982,8 +982,8 @@ class ProjectPushItemProcess: def _make_sure_task_exists( self, - folder_entity: Dict[str, Any], - task_info: Dict[str, Any], + folder_entity: dict[str, Any], + task_info: dict[str, Any], ): """Creates destination task from source task information""" project_name = self._item.dst_project_name @@ -1326,6 +1326,6 @@ class IntegrateModel: return item.integrate() - def get_items(self) -> Dict[str, ProjectPushItemProcess]: + def get_items(self) -> dict[str, ProjectPushItemProcess]: """Returns dict of all ProjectPushItemProcess items """ return self._process_items From 7b5ca16993bbc844d14ca27bc042e23ee58ce9bf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 17:03:50 +0200 Subject: [PATCH 44/95] Use lower only for comparison Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 1ecf8a8a59..1cd9e2deaf 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -782,8 +782,8 @@ class ProjectPushItemProcess: self._task_info = {} return - dst_task_name = src_task_info["name"].lower() - if dst_task_name not in folder_tasks: + dst_task_name = src_task_info["name"] + if dst_task_name.lower() not in folder_tasks: self._make_sure_task_exists( folder_entity, src_task_info ) From 67994bb5a3bc8dc527a4fd09f6110049c551238e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 17:04:07 +0200 Subject: [PATCH 45/95] Remove unnecessary variable Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 1cd9e2deaf..2030027bb0 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -987,7 +987,7 @@ class ProjectPushItemProcess: ): """Creates destination task from source task information""" project_name = self._item.dst_project_name - _task_id = self._operations.create_task( + self._operations.create_task( project_name, task_info["name"], folder_id=folder_entity["id"], From cea56fbe5322075dbdb0826e78d0b2a27069b5ac Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 17:04:32 +0200 Subject: [PATCH 46/95] Formatting change Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 2030027bb0..7e92d82f41 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1002,7 +1002,7 @@ class ProjectPushItemProcess: ) if not src_task: self._status.set_failed( - f"No task selected and couldn't find source task" + "No task selected and couldn't find source task" ) raise PushToProjectError(self._status.fail_reason) return src_task From 04322ef94d673cd3b4356afb6897ec7d30d8d8bb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 17:08:35 +0200 Subject: [PATCH 47/95] Removed hard fail, unnecessary --- client/ayon_core/tools/push_to_project/models/integrate.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 5127afd0ee..164b73e0ef 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1020,11 +1020,7 @@ class ProjectPushItemProcess: src_task = ayon_api.get_task_by_id( self._item.src_project_name, src_version_entity["taskId"] ) - if not src_task: - self._status.set_failed( - f"No task selected and couldn't find source task" - ) - raise PushToProjectError(self._status.fail_reason) + return src_task def _integrate_representations(self): From 42722c08960e528a9e9cfa735aedfd7023533bde Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 23 Oct 2025 17:20:16 +0200 Subject: [PATCH 48/95] Added validation that task type is in destination project --- .../tools/push_to_project/models/integrate.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 164b73e0ef..e80a525204 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1007,11 +1007,26 @@ class ProjectPushItemProcess: ): """Creates destination task from source task information""" project_name = self._item.dst_project_name + found_task_type = False + src_task_type = task_info["taskType"] + for task_type in self._project_entity["taskTypes"]: + if task_type["name"].lower() == src_task_type.lower(): + found_task_type = True + break + + if not found_task_type: + self._status.set_failed( + f"'{src_task_type}' task type is not configured in " + "project Anatomy." + ) + + raise PushToProjectError(self._status.fail_reason) + _task_id = self._operations.create_task( project_name, task_info["name"], folder_id=folder_entity["id"], - task_type=task_info["taskType"], + task_type=src_task_type, attrib=task_info["attrib"], ) From 7e3e5855b86a31e1206c199478bddeb9829a6c80 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 11:01:24 +0200 Subject: [PATCH 49/95] Fix use of lower task name --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 5370a35c37..a98c045893 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -808,7 +808,7 @@ class ProjectPushItemProcess: folder_entity, src_task_info ) task_info = copy.deepcopy(src_task_info) - folder_tasks[dst_task_name] = task_info + folder_tasks[dst_task_name.lower()] = task_info task_info = folder_tasks.get(dst_task_name.lower()) if not task_info: From f33b13c19449aed3ab9b1aa9fb45716c6418d52e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 11:22:13 +0200 Subject: [PATCH 50/95] Fix if source version doesn't have task Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index a98c045893..a8cd3be2cc 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1032,6 +1032,8 @@ class ProjectPushItemProcess: def _get_src_task_info(self): src_version_entity = self._src_version_entity + if not src_version_entity["taskId"]: + return None src_task = ayon_api.get_task_by_id( self._item.src_project_name, src_version_entity["taskId"] ) From f6e4d50137f4dbcbc830639bc6e73efc054d27e1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 12:19:31 +0200 Subject: [PATCH 51/95] Fix overwriting real task name with name of task type --- client/ayon_core/tools/push_to_project/models/integrate.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index a98c045893..5be7dbe2c1 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -828,7 +828,10 @@ class ProjectPushItemProcess: task_type["name"]: task_type for task_type in self._project_entity["taskTypes"] } - task_type_info = task_types_by_name.get(task_type_name, {}) + task_type_info = copy.deepcopy( + task_types_by_name.get(task_type_name, {}) + ) + task_type_info.pop("name") # do not overwrite real task name task_info.update(task_type_info) self._task_info = task_info From efec97fda3938f883b166fa4de26b127fc488920 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 12:20:14 +0200 Subject: [PATCH 52/95] Return task info from created object --- .../ayon_core/tools/push_to_project/models/integrate.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 5be7dbe2c1..2d02316db0 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -804,10 +804,9 @@ class ProjectPushItemProcess: dst_task_name = src_task_info["name"] if dst_task_name.lower() not in folder_tasks: - self._make_sure_task_exists( + task_info = self._make_sure_task_exists( folder_entity, src_task_info ) - task_info = copy.deepcopy(src_task_info) folder_tasks[dst_task_name.lower()] = task_info task_info = folder_tasks.get(dst_task_name.lower()) @@ -1007,7 +1006,7 @@ class ProjectPushItemProcess: self, folder_entity: dict[str, Any], task_info: dict[str, Any], - ): + ) -> dict[str, Any]: """Creates destination task from source task information""" project_name = self._item.dst_project_name found_task_type = False @@ -1025,13 +1024,15 @@ class ProjectPushItemProcess: raise PushToProjectError(self._status.fail_reason) - self._operations.create_task( + task_info = self._operations.create_task( project_name, task_info["name"], folder_id=folder_entity["id"], task_type=src_task_type, attrib=task_info["attrib"], ) + self._task_info = task_info.data + return self._task_info def _get_src_task_info(self): src_version_entity = self._src_version_entity From 636ef024b786dafe45233522aea2d92ecccd8440 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 12:20:34 +0200 Subject: [PATCH 53/95] Task is optional --- client/ayon_core/tools/push_to_project/models/integrate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 2d02316db0..fc61204bf3 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -965,8 +965,8 @@ class ProjectPushItemProcess: version = get_versioning_start( project_name, self.host_name, - task_name=self._task_info["name"], - task_type=self._task_info["taskType"], + task_name=self._task_info.get("name"), + task_type=self._task_info.get("taskType"), product_type=product_type, product_name=product_entity["name"], ) From 49162f228e9cb3bd8ceee2bad8ad54ca665b4c75 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 12:20:54 +0200 Subject: [PATCH 54/95] Fix pushed products not attaching to version --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index fc61204bf3..45035671b2 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -994,6 +994,7 @@ class ProjectPushItemProcess: version_entity = new_version_entity( version, product_id, + task_id=self._task_info.get("id"), attribs=dst_attrib, thumbnail_id=thumbnail_id, ) From fcc82a8e463388e205aac4460a73fbff16552386 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 14:14:21 +0200 Subject: [PATCH 55/95] Transfer status and tags --- .../tools/push_to_project/models/integrate.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index f8360c520b..8c125dd3dc 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -990,10 +990,15 @@ class ProjectPushItemProcess: existing_version_entity["attrib"].update(dst_attrib) self._version_entity = existing_version_entity return + copied_tags = self._get_transferable_tags(src_version_entity) + copied_status = self._get_transferable_status(src_version_entity) version_entity = new_version_entity( version, product_id, + author=src_version_entity["author"], + status=copied_status, + tags=copied_tags, task_id=self._task_info.get("id"), attribs=dst_attrib, thumbnail_id=thumbnail_id, @@ -1291,6 +1296,30 @@ class ProjectPushItemProcess: repre_context.pop("task", None) return repre_context + def _get_transferable_tags(self, src_version_entity): + """Copy over only tags present in destination project""" + dst_project_tags = [ + tag["name"] for tag in self._project_entity["tags"] + ] + copied_tags = [] + for src_tag in src_version_entity["tags"]: + if src_tag in dst_project_tags: + copied_tags.append(src_tag) + return copied_tags + + def _get_transferable_status(self, src_version_entity): + """Copy over status, first status if not matching found""" + dst_project_statuses = { + status["name"]: status + for status in self._project_entity["statuses"] + } + copied_status = dst_project_statuses.get(src_version_entity["status"]) + if not copied_status: + copied_status = dst_project_statuses[ + dst_project_statuses.keys()[0] + ] + return copied_status["name"] + class IntegrateModel: def __init__(self, controller): From 3104e07c78dd62ff438b122c97dbe63cf1e3b665 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 15:30:52 +0200 Subject: [PATCH 56/95] Fix access to dict keys --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 8c125dd3dc..838bf079ec 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1316,7 +1316,7 @@ class ProjectPushItemProcess: copied_status = dst_project_statuses.get(src_version_entity["status"]) if not copied_status: copied_status = dst_project_statuses[ - dst_project_statuses.keys()[0] + next(iter(dst_project_statuses)) ] return copied_status["name"] From 542acd0896e1ad266f7764811817212dc4ab6d06 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 15:33:15 +0200 Subject: [PATCH 57/95] Fix access to dict keys --- client/ayon_core/tools/push_to_project/models/integrate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 838bf079ec..e23d2a8eb2 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1313,8 +1313,13 @@ class ProjectPushItemProcess: status["name"]: status for status in self._project_entity["statuses"] } - copied_status = dst_project_statuses.get(src_version_entity["status"]) + source_status = src_version_entity["status"] + copied_status = dst_project_statuses.get(source_status) if not copied_status: + self._log_warning( + f"'{source_status}' not found in destination project. " + "Used first configured status from there." + ) copied_status = dst_project_statuses[ next(iter(dst_project_statuses)) ] From fcebdaf13006aeeefd2979ceac24304dee9cd618 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 24 Oct 2025 16:00:42 +0200 Subject: [PATCH 58/95] Do not send dummy status if not found --- .../tools/push_to_project/models/integrate.py | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index e23d2a8eb2..2adc708cf3 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1284,10 +1284,12 @@ class ProjectPushItemProcess: if context_value and isinstance(context_value, dict): for context_sub_key in context_value.keys(): value_to_update = formatting_data.get(context_key, {}).get( - context_sub_key) + context_sub_key + ) if value_to_update: - repre_context[context_key][ - context_sub_key] = value_to_update + repre_context[context_key][context_sub_key] = ( + value_to_update + ) else: value_to_update = formatting_data.get(context_key) if value_to_update: @@ -1313,17 +1315,10 @@ class ProjectPushItemProcess: status["name"]: status for status in self._project_entity["statuses"] } - source_status = src_version_entity["status"] - copied_status = dst_project_statuses.get(source_status) - if not copied_status: - self._log_warning( - f"'{source_status}' not found in destination project. " - "Used first configured status from there." - ) - copied_status = dst_project_statuses[ - next(iter(dst_project_statuses)) - ] - return copied_status["name"] + copied_status = dst_project_statuses.get(src_version_entity["status"]) + if copied_status: + return copied_status["name"] + return None class IntegrateModel: From 3ee7c30cae8b75726875e58970f19fe08af14ee4 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Fri, 24 Oct 2025 17:35:28 +0200 Subject: [PATCH 59/95] Handles missing media references in OTIO clips Adds a check for missing media references in OTIO clips during publishing. --- .../ayon_core/plugins/publish/collect_otio_frame_ranges.py | 6 ++++++ .../plugins/publish/collect_otio_subset_resources.py | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py index d68970d428..543277f37e 100644 --- a/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py +++ b/client/ayon_core/plugins/publish/collect_otio_frame_ranges.py @@ -71,6 +71,12 @@ class CollectOtioRanges(pyblish.api.InstancePlugin): import opentimelineio as otio otio_clip = instance.data["otioClip"] + if isinstance( + otio_clip.media_reference, + otio.schema.MissingReference + ): + self.log.info("Clip has no media reference") + return # Collect timeline ranges if workfile start frame is available if "workfileFrameStart" in instance.data: diff --git a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py index 275b8a7f55..4d3c1cfb13 100644 --- a/client/ayon_core/plugins/publish/collect_otio_subset_resources.py +++ b/client/ayon_core/plugins/publish/collect_otio_subset_resources.py @@ -60,6 +60,13 @@ class CollectOtioSubsetResources( # get basic variables otio_clip = instance.data["otioClip"] + if isinstance( + otio_clip.media_reference, + otio.schema.MissingReference + ): + self.log.info("Clip has no media reference") + return + otio_available_range = otio_clip.available_range() media_fps = otio_available_range.start_time.rate available_duration = otio_available_range.duration.value From 373683890c07ac137df771c5e5d58c82f00fe87d Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Mon, 27 Oct 2025 10:05:30 +0100 Subject: [PATCH 60/95] Use correct publish template in `get_instance_expected_output_path` --- client/ayon_core/pipeline/publish/lib.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/publish/lib.py b/client/ayon_core/pipeline/publish/lib.py index fb84417730..f6198bd45e 100644 --- a/client/ayon_core/pipeline/publish/lib.py +++ b/client/ayon_core/pipeline/publish/lib.py @@ -955,7 +955,26 @@ def get_instance_expected_output_path( "version": version }) - path_template_obj = anatomy.get_template_item("publish", "default")["path"] + # Get instance publish template 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"] + + template_name = get_publish_template_name( + project_name=instance.context.data["projectName"], + host_name=instance.context.data["hostName"], + product_type=instance.data["productType"], + task_name=task_name, + task_type=task_type, + project_settings=instance.context.data["project_settings"], + ) + + path_template_obj = anatomy.get_template_item( + "publish", + template_name + )["path"] template_filled = path_template_obj.format_strict(template_data) return os.path.normpath(template_filled) From a162d6bce1c4ec9fe20c918d50bc831ffeb54e2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:50:13 +0100 Subject: [PATCH 61/95] fix mytasks filtering --- client/ayon_core/tools/workfiles/widgets/window.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index 3f96f0bb15..c7ff98f25e 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -358,9 +358,8 @@ class WorkfilesToolWindow(QtWidgets.QWidget): if not self._host_is_valid: return - self._folders_widget.set_project_name( - self._controller.get_current_project_name() - ) + self._project_name = self._controller.get_current_project_name() + self._folders_widget.set_project_name(self._project_name) def _on_save_as_finished(self, event): if event["failed"]: @@ -412,6 +411,7 @@ class WorkfilesToolWindow(QtWidgets.QWidget): entity_ids = self._controller.get_my_tasks_entity_ids( self._project_name ) + print(entity_ids) folder_ids = entity_ids["folder_ids"] task_ids = entity_ids["task_ids"] self._folders_widget.set_folder_ids_filter(folder_ids) From c03fe908a74b0d5e815f94526363980097bbe676 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Oct 2025 15:57:12 +0100 Subject: [PATCH 62/95] lock pyobjc-core to 11.1 --- client/pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/pyproject.toml b/client/pyproject.toml index 6416d9b8e1..c98591b707 100644 --- a/client/pyproject.toml +++ b/client/pyproject.toml @@ -19,3 +19,6 @@ OpenTimelineIO = "0.16.0" opencolorio = "^2.3.2,<2.4.0" Pillow = "9.5.0" websocket-client = ">=0.40.0,<2" + +[ayon.runtimeDependencies.darwin] +pyobjc-core = "^11.1" From 5d74d9dc514b5ca8b4b4298c1346222c18ac0ca4 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Mon, 27 Oct 2025 16:52:18 +0100 Subject: [PATCH 63/95] Remove dev print --- client/ayon_core/tools/workfiles/widgets/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/workfiles/widgets/window.py b/client/ayon_core/tools/workfiles/widgets/window.py index c7ff98f25e..00362ea866 100644 --- a/client/ayon_core/tools/workfiles/widgets/window.py +++ b/client/ayon_core/tools/workfiles/widgets/window.py @@ -411,7 +411,6 @@ class WorkfilesToolWindow(QtWidgets.QWidget): entity_ids = self._controller.get_my_tasks_entity_ids( self._project_name ) - print(entity_ids) folder_ids = entity_ids["folder_ids"] task_ids = entity_ids["task_ids"] self._folders_widget.set_folder_ids_filter(folder_ids) From 425dbc6db1addf6cb603d0774f5aa6abf4637a01 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 27 Oct 2025 18:07:49 +0100 Subject: [PATCH 64/95] Implemented copy of source folder thumbnail --- .../tools/push_to_project/models/integrate.py | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 2adc708cf3..8a6000122f 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -3,6 +3,7 @@ import re import copy import itertools import sys +import tempfile import traceback import uuid from typing import Optional, Any @@ -709,11 +710,14 @@ class ProjectPushItemProcess: project_entity, src_folder_type ) + new_thumbnail_id = self._get_new_folder_thumbnail_id( + project_entity, src_folder_entity) folder_entity = new_folder_entity( folder_name, dst_folder_type, parent_id=parent_id, - attribs=new_folder_attrib + attribs=new_folder_attrib, + thumbnail_id=new_thumbnail_id ) if folder_label: folder_entity["label"] = folder_label @@ -733,6 +737,36 @@ class ProjectPushItemProcess: folder_entity["path"] = "/".join([parent_path, folder_name]) return folder_entity + def _get_new_folder_thumbnail_id( + self, + project_entity: dict[str, Any], + src_folder_entity: dict[str, Any] + ) -> Optional[str]: + """Copy thumbnail possibly set on folder. + + Could be different from representation thumbnails, and it is only shown + when folder is selected. + """ + new_thumbnail_id = None + if src_folder_entity["thumbnailId"]: + thumbnail = ayon_api.get_thumbnail_by_id( + self._item.src_project_name, src_folder_entity["thumbnailId"] + ) + if not thumbnail.id: + return new_thumbnail_id + + try: + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(thumbnail.content) + temp_file_path = tmp_file.name + + new_thumbnail_id = ayon_api.create_thumbnail( + project_entity["name"], temp_file_path) + finally: + if temp_file_path and os.path.exists(temp_file_path): + os.remove(temp_file_path) + return new_thumbnail_id + def _get_dst_folder_type( self, project_entity: dict[str, Any], From e184c1b3dd5de7fbd95bcf7afe643c1738659e2c Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 29 Oct 2025 14:22:29 +0100 Subject: [PATCH 65/95] don't require 'AYON_STUDIO_BUNDLE_NAME' to be set --- client/ayon_core/addon/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index 9207bb74c0..a04aedb8cc 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -141,6 +141,9 @@ def _get_ayon_bundle_data() -> tuple[ ]: studio_bundle_name = os.environ.get("AYON_STUDIO_BUNDLE_NAME") project_bundle_name = os.getenv("AYON_BUNDLE_NAME") + # If AYON launcher <1.4.0 was used + if not studio_bundle_name: + studio_bundle_name = project_bundle_name bundles = ayon_api.get_bundles()["bundles"] studio_bundle = next( ( From 9d3585a0c0d73aae50ab2dd444fa1e0aea3bec71 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Oct 2025 15:12:38 +0100 Subject: [PATCH 66/95] Renamed method Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 8a6000122f..0968b99eb5 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -737,7 +737,7 @@ class ProjectPushItemProcess: folder_entity["path"] = "/".join([parent_path, folder_name]) return folder_entity - def _get_new_folder_thumbnail_id( + def _create_new_folder_thumbnail( self, project_entity: dict[str, Any], src_folder_entity: dict[str, Any] From 6dc68606222c88ffaac871bc25f68e5fe0856a19 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Oct 2025 15:13:35 +0100 Subject: [PATCH 67/95] Reorganized flow Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- .../tools/push_to_project/models/integrate.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 0968b99eb5..33eac6c3d6 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -747,24 +747,28 @@ class ProjectPushItemProcess: Could be different from representation thumbnails, and it is only shown when folder is selected. """ + if not src_folder_entity["thumbnailId"]: + return None + + thumbnail = ayon_api.get_folder_thumbnail( + self._item.src_project_name, + src_folder_entity["id"], + src_folder_entity["thumbnailId"] + ) + if not thumbnail.id: + return None + + with tempfile.NamedTemporaryFile(delete=False) as tmp_file: + tmp_file.write(thumbnail.content) + temp_file_path = tmp_file.name + new_thumbnail_id = None - if src_folder_entity["thumbnailId"]: - thumbnail = ayon_api.get_thumbnail_by_id( - self._item.src_project_name, src_folder_entity["thumbnailId"] - ) - if not thumbnail.id: - return new_thumbnail_id - - try: - with tempfile.NamedTemporaryFile(delete=False) as tmp_file: - tmp_file.write(thumbnail.content) - temp_file_path = tmp_file.name - - new_thumbnail_id = ayon_api.create_thumbnail( - project_entity["name"], temp_file_path) - finally: - if temp_file_path and os.path.exists(temp_file_path): - os.remove(temp_file_path) + try: + new_thumbnail_id = ayon_api.create_thumbnail( + project_entity["name"], temp_file_path) + finally: + if os.path.exists(temp_file_path): + os.remove(temp_file_path) return new_thumbnail_id def _get_dst_folder_type( From 35926269a624b8bbde492defa6aaf756e10c8316 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 29 Oct 2025 15:14:38 +0100 Subject: [PATCH 68/95] Used renamed method --- client/ayon_core/tools/push_to_project/models/integrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 33eac6c3d6..cacce44942 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -710,7 +710,7 @@ class ProjectPushItemProcess: project_entity, src_folder_type ) - new_thumbnail_id = self._get_new_folder_thumbnail_id( + new_thumbnail_id = self._create_new_folder_thumbnail( project_entity, src_folder_entity) folder_entity = new_folder_entity( folder_name, From e5265ccdc01829ee817327368a164778a40ad155 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 29 Oct 2025 15:31:57 +0000 Subject: [PATCH 69/95] [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 8e0834b8da..e40a2e3663 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.6+dev" +__version__ = "1.6.7" diff --git a/package.py b/package.py index 5fa4d165d2..8cd5df8dfc 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.6+dev" +version = "1.6.7" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 73b9a4a916..11e7d4d3c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.6+dev" +version = "1.6.7" description = "" authors = ["Ynput Team "] readme = "README.md" From 757d42148e7476f4065fb4418c2d129319236090 Mon Sep 17 00:00:00 2001 From: Ynbot Date: Wed, 29 Oct 2025 15:32:36 +0000 Subject: [PATCH 70/95] [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 e40a2e3663..6aa30b935a 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.7" +__version__ = "1.6.7+dev" diff --git a/package.py b/package.py index 8cd5df8dfc..ff3fad5b19 100644 --- a/package.py +++ b/package.py @@ -1,6 +1,6 @@ name = "core" title = "Core" -version = "1.6.7" +version = "1.6.7+dev" client_dir = "ayon_core" diff --git a/pyproject.toml b/pyproject.toml index 11e7d4d3c2..6656f15249 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ [tool.poetry] name = "ayon-core" -version = "1.6.7" +version = "1.6.7+dev" description = "" authors = ["Ynput Team "] readme = "README.md" From 5cd46678b473505384fa8b0b1ba061adab671bd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Oct 2025 15:33:32 +0000 Subject: [PATCH 71/95] 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 60693f088d..c79ca69fca 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.7 - 1.6.6 - 1.6.5 - 1.6.4 From b3dbee7664f23289f8507a375fe8fae063817800 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Oct 2025 16:43:38 -0400 Subject: [PATCH 72/95] Fix legacy OTIO clips detection on range remap. --- client/ayon_core/pipeline/editorial.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index 716035aa1c..a53f1b5ae5 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -250,21 +250,17 @@ def remap_range_on_file_sequence(otio_clip, otio_range): if ( is_clip_from_media_sequence(otio_clip) and available_range_start_frame == media_ref.start_frame - and conformed_src_in.to_frames() < media_ref.start_frame + and round(conformed_src_in.value) < media_ref.start_frame ): media_in = otio.opentime.RationalTime( 0, rate=available_range_rate ) src_offset_in = otio_range.start_time - media_in - # make sure that only if any offset is present - if media_ref.start_frame == src_offset_in.to_frames(): - frame_in = src_offset_in.to_frames() - else: - frame_in = otio.opentime.RationalTime.from_frames( - media_ref.start_frame + src_offset_in.to_frames(), + frame_in = otio.opentime.RationalTime.from_frames( + media_ref.start_frame + src_offset_in.to_frames(), rate=available_range_rate, - ).to_frames() + ).to_frames() # e.g.: # duration = 10 frames at 24fps From 9eef269aafa5d3e79e94b69e4938375b55805f53 Mon Sep 17 00:00:00 2001 From: "robin@ynput.io" Date: Wed, 29 Oct 2025 16:57:49 -0400 Subject: [PATCH 73/95] Add comment. --- client/ayon_core/pipeline/editorial.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/ayon_core/pipeline/editorial.py b/client/ayon_core/pipeline/editorial.py index a53f1b5ae5..21468e6ddd 100644 --- a/client/ayon_core/pipeline/editorial.py +++ b/client/ayon_core/pipeline/editorial.py @@ -250,6 +250,10 @@ def remap_range_on_file_sequence(otio_clip, otio_range): if ( is_clip_from_media_sequence(otio_clip) and available_range_start_frame == media_ref.start_frame + + # source range should be included in available range from media + # using round instead of conformed_src_in.to_frames() to avoid + # any precision issue with frame rate. and round(conformed_src_in.value) < media_ref.start_frame ): media_in = otio.opentime.RationalTime( From cb81a57dddf6024318af16dfbde4a7e63cf7546a Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Oct 2025 22:31:49 +0100 Subject: [PATCH 74/95] Allow visualizing Alpha only layers for review as a color matte --- client/ayon_core/lib/transcoding.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 1762881846..38033d084f 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -487,6 +487,11 @@ def get_review_info_by_layer_name(channel_names): elif "Z" in channel_info: rgb = "Z", "Z", "Z" + # Has only A channel (Alpha layer) + elif "A" in channel_info: + rgb = "A", "A", "A" + alpha = None + else: # No reviewable channels found continue From 66c6bdd96025f94ddb39fbcd5b442bd9d9f736c2 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 30 Oct 2025 22:57:26 +0100 Subject: [PATCH 75/95] Do not fail on thumbnail creation if it can't resolve a reviewable channel with OIIO --- .../ayon_core/plugins/publish/extract_thumbnail.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/extract_thumbnail.py b/client/ayon_core/plugins/publish/extract_thumbnail.py index b5885178d0..2a43c12af3 100644 --- a/client/ayon_core/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/plugins/publish/extract_thumbnail.py @@ -17,6 +17,7 @@ from ayon_core.lib import ( run_subprocess, ) from ayon_core.lib.transcoding import ( + MissingRGBAChannelsError, oiio_color_convert, get_oiio_input_and_channel_args, get_oiio_info_for_input, @@ -477,7 +478,16 @@ class ExtractThumbnail(pyblish.api.InstancePlugin): return False input_info = get_oiio_info_for_input(src_path, logger=self.log) - input_arg, channels_arg = get_oiio_input_and_channel_args(input_info) + try: + input_arg, channels_arg = get_oiio_input_and_channel_args( + input_info + ) + except MissingRGBAChannelsError: + self.log.debug( + "Unable to find relevant reviewable channel for thumbnail " + "creation" + ) + return False oiio_cmd = get_oiio_tool_args( "oiiotool", input_arg, src_path, From 5e877f9b05730ac9bc704d2741614e6fbeef9837 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 15:10:03 +0100 Subject: [PATCH 76/95] Copy over attrib to copy product groupping --- client/ayon_core/tools/push_to_project/models/integrate.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index cacce44942..761eb175cd 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -952,6 +952,7 @@ class ProjectPushItemProcess: product_name, product_type, folder_id, + attribs=self._src_product_entity["attrib"] ) self._operations.create_entity( project_name, "product", product_entity From 758e232b6c14b872f27b3d5a22bda7fc4db762a0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 15:35:27 +0100 Subject: [PATCH 77/95] Copy over only limited set of attributes for safety --- client/ayon_core/tools/push_to_project/models/integrate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index 761eb175cd..a72a74f805 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -948,11 +948,16 @@ class ProjectPushItemProcess: self._product_entity = product_entity return product_entity + src_attrib = self._src_product_entity["attrib"] + copied_attrib = { + "description": src_attrib.get("description"), + "productGroup": src_attrib.get("productGroup") + } product_entity = new_product_entity( product_name, product_type, folder_id, - attribs=self._src_product_entity["attrib"] + attribs=copied_attrib ) self._operations.create_entity( project_name, "product", product_entity From 43f7ace90e298c59a06983cc4857426ab85c1629 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 15:58:36 +0100 Subject: [PATCH 78/95] Make copy of selected attributes closer to existing --- .../tools/push_to_project/models/integrate.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index a72a74f805..b2a987e121 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -949,15 +949,20 @@ class ProjectPushItemProcess: return product_entity src_attrib = self._src_product_entity["attrib"] - copied_attrib = { - "description": src_attrib.get("description"), - "productGroup": src_attrib.get("productGroup") - } + + dst_attrib = {} + for key in { + "description", + "productGroup", + }: + if key in src_attrib: + dst_attrib[key] = src_attrib[key] + product_entity = new_product_entity( product_name, product_type, folder_id, - attribs=copied_attrib + attribs=dst_attrib ) self._operations.create_entity( project_name, "product", product_entity From 23fd59f23ae728b2eadf143ab938c0bec737441f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:04:23 +0100 Subject: [PATCH 79/95] Added version_up checkbox If selected it creates new version for existing product, otherwise it overwrites the version. --- .../tools/push_to_project/control.py | 1 + .../tools/push_to_project/ui/window.py | 22 ++++++++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index b4e0d56dfd..41cd9bf823 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -41,6 +41,7 @@ class PushToContextController: self._process_item_id = None self._use_original_name = False + self._version_up = False self.set_source(project_name, version_ids) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index f382ccce64..c392c6b519 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -153,7 +153,12 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout.addRow("New folder name", folder_name_input) inputs_layout.addRow("Variant", variant_input) inputs_layout.addRow( - "Use original product names", original_names_checkbox) + "Use original product names", original_names_checkbox + ) + version_up_checkbox = NiceCheckbox(True, parent=inputs_widget) + inputs_layout.addRow( + "Version up existing Product", version_up_checkbox + ) inputs_layout.addRow("Comment", comment_input) main_splitter.addWidget(context_widget) @@ -209,8 +214,11 @@ class PushToContextSelectWindow(QtWidgets.QWidget): "Show error detail dialog to copy full error." ) original_names_checkbox.setToolTip( - "Required for multi copy, doesn't allow changes " - "variant values." + "Required for multi copy, doesn't allow changes variant values." + ) + version_up_checkbox.setToolTip( + "Version up existing product. If not selected version will be " + "updated." ) overlay_close_btn = QtWidgets.QPushButton( @@ -259,6 +267,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): library_only_checkbox.stateChanged.connect(self._on_library_only_change) original_names_checkbox.stateChanged.connect( self._on_original_names_change) + version_up_checkbox.stateChanged.connect( + self._on_version_up_checkbox_change) publish_btn.clicked.connect(self._on_select_click) cancel_btn.clicked.connect(self._on_close_click) @@ -328,6 +338,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._new_folder_name_input_text = None self._variant_input_text = None self._comment_input_text = None + self._version_up_checkbox = version_up_checkbox self._first_show = True self._show_timer = show_timer @@ -344,6 +355,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): show_detail_btn.setVisible(False) overlay_close_btn.setVisible(False) overlay_try_btn.setVisible(False) + version_up_checkbox.setChecked(False) # Support of public api function of controller def set_source(self, project_name, version_ids): @@ -424,6 +436,10 @@ class PushToContextSelectWindow(QtWidgets.QWidget): use_original_name = bool(state) self._invalidate_use_original_names(use_original_name) + def _on_version_up_checkbox_change(self, state: int) -> None: + self._controller._version_up = bool(state) + self._version_up_checkbox.setChecked(bool(state)) + def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled folder_name = self._new_folder_name_input_text From 8bb4b2096a66cfd678c29c8c4b816c0140ad3295 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:04:54 +0100 Subject: [PATCH 80/95] Implemented propagation of version up value --- client/ayon_core/tools/push_to_project/control.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 41cd9bf823..56e587a4e2 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -203,6 +203,9 @@ class PushToContextController: return item_ids = [] + dst_version = 1 + if self._version_up: + dst_version = None for src_version_entity in self._src_version_entities: item_id = self._integrate_model.create_process_item( self._src_project_name, @@ -213,7 +216,7 @@ class PushToContextController: self._user_values.variant, comment=self._user_values.comment, new_folder_name=self._user_values.new_folder_name, - dst_version=1, + dst_version=dst_version, use_original_name=self._use_original_name, ) item_ids.append(item_id) From 23a6578d6fc0235b8ba2ec3b75ddef8695373bc9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:09:59 +0100 Subject: [PATCH 81/95] Formatting change --- .../tools/push_to_project/models/integrate.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index b2a987e121..8dd8d8145c 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -955,8 +955,9 @@ class ProjectPushItemProcess: "description", "productGroup", }: - if key in src_attrib: - dst_attrib[key] = src_attrib[key] + value = src_attrib.get(key) + if value: + dst_attrib[key] = value product_entity = new_product_entity( product_name, @@ -1001,8 +1002,9 @@ class ProjectPushItemProcess: "description", "intent", }: - if key in src_attrib: - dst_attrib[key] = src_attrib[key] + value = src_attrib.get(key) + if value: + dst_attrib[key] = value if version is None: last_version_entity = ayon_api.get_last_version_by_product_id( From 3bc92d88f04db94c9ffb9f7cbf9b2c1bd866a87c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:13:48 +0100 Subject: [PATCH 82/95] Reorganized lines --- client/ayon_core/tools/push_to_project/ui/window.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index c392c6b519..7eb90425ab 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -144,6 +144,8 @@ class PushToContextSelectWindow(QtWidgets.QWidget): variant_input.setPlaceholderText("< Variant >") variant_input.setObjectName("ValidatedLineEdit") + version_up_checkbox = NiceCheckbox(True, parent=inputs_widget) + comment_input = PlaceholderLineEdit(inputs_widget) comment_input.setPlaceholderText("< Publish comment >") @@ -155,7 +157,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): inputs_layout.addRow( "Use original product names", original_names_checkbox ) - version_up_checkbox = NiceCheckbox(True, parent=inputs_widget) inputs_layout.addRow( "Version up existing Product", version_up_checkbox ) From 9713852deb28a5136c54156803723929f4067365 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:15:19 +0100 Subject: [PATCH 83/95] Unnecessary and wrong manual set --- client/ayon_core/tools/push_to_project/ui/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 7eb90425ab..23aa710cd3 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -439,7 +439,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): def _on_version_up_checkbox_change(self, state: int) -> None: self._controller._version_up = bool(state) - self._version_up_checkbox.setChecked(bool(state)) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From 23b0378a0e0fffc68f206c7800f1552d9f9e764f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:17:49 +0100 Subject: [PATCH 84/95] Use public method to set private variable --- client/ayon_core/tools/push_to_project/control.py | 3 +++ client/ayon_core/tools/push_to_project/ui/window.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 56e587a4e2..8fd4b6053e 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -233,6 +233,9 @@ class PushToContextController: thread.start() return item_ids + def set_version_up(self, state): + self._version_up = state + def wait_for_process_thread(self): if self._process_thread is None: return diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index 23aa710cd3..6be4d3c237 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -438,7 +438,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._invalidate_use_original_names(use_original_name) def _on_version_up_checkbox_change(self, state: int) -> None: - self._controller._version_up = bool(state) + self._controller.set_version_up(bool(state)) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From 7229f5d794861e71842fd6c1d20707ea174c02a9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 16:57:50 +0100 Subject: [PATCH 85/95] Reworked hardcoded version to version_up variable 1 was used as hardcoded version, this way updated will be always last version if exists. Hardcoding 1 doesnt make sense with `get_versioning_start` which should be source of truth. Incoming value of version would make sense if we would like to start/reset specific version, which is unlikely (and currently impossible without updates to UI). --- .../tools/push_to_project/control.py | 5 +- .../tools/push_to_project/models/integrate.py | 49 ++++++++++--------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 8fd4b6053e..a24cedf455 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -203,9 +203,6 @@ class PushToContextController: return item_ids = [] - dst_version = 1 - if self._version_up: - dst_version = None for src_version_entity in self._src_version_entities: item_id = self._integrate_model.create_process_item( self._src_project_name, @@ -216,7 +213,7 @@ class PushToContextController: self._user_values.variant, comment=self._user_values.comment, new_folder_name=self._user_values.new_folder_name, - dst_version=dst_version, + version_up=self._version_up, use_original_name=self._use_original_name, ) item_ids.append(item_id) diff --git a/client/ayon_core/tools/push_to_project/models/integrate.py b/client/ayon_core/tools/push_to_project/models/integrate.py index cacce44942..7130922ba0 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -89,7 +89,7 @@ class ProjectPushItem: variant, comment, new_folder_name, - dst_version, + version_up, item_id=None, use_original_name=False ): @@ -100,7 +100,7 @@ class ProjectPushItem: self.dst_project_name = dst_project_name self.dst_folder_id = dst_folder_id self.dst_task_name = dst_task_name - self.dst_version = dst_version + self.version_up = version_up self.variant = variant self.new_folder_name = new_folder_name self.comment = comment or "" @@ -118,7 +118,7 @@ class ProjectPushItem: str(self.dst_folder_id), str(self.new_folder_name), str(self.dst_task_name), - str(self.dst_version), + str(self.version_up), self.use_original_name ]) return self._repr_value @@ -133,7 +133,7 @@ class ProjectPushItem: "dst_project_name": self.dst_project_name, "dst_folder_id": self.dst_folder_id, "dst_task_name": self.dst_task_name, - "dst_version": self.dst_version, + "version_up": self.version_up, "variant": self.variant, "comment": self.comment, "new_folder_name": self.new_folder_name, @@ -962,7 +962,7 @@ class ProjectPushItemProcess: """Make sure version document exits in database.""" project_name = self._item.dst_project_name - version = self._item.dst_version + version_up = self._item.version_up src_version_entity = self._src_version_entity product_entity = self._product_entity product_id = product_entity["id"] @@ -993,24 +993,25 @@ class ProjectPushItemProcess: if key in src_attrib: dst_attrib[key] = src_attrib[key] - if version is None: - last_version_entity = ayon_api.get_last_version_by_product_id( - project_name, product_id + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, product_id + ) + if last_version_entity is None: + dst_version = get_versioning_start( + project_name, + self.host_name, + task_name=self._task_info.get("name"), + task_type=self._task_info.get("taskType"), + product_type=product_type, + product_name=product_entity["name"], ) - if last_version_entity: - version = int(last_version_entity["version"]) + 1 - else: - version = get_versioning_start( - project_name, - self.host_name, - task_name=self._task_info.get("name"), - task_type=self._task_info.get("taskType"), - product_type=product_type, - product_name=product_entity["name"], - ) + else: + dst_version = int(last_version_entity["version"]) + if version_up: + dst_version += 1 existing_version_entity = ayon_api.get_version_by_name( - project_name, version, product_id + project_name, dst_version, product_id ) thumbnail_id = self._copy_version_thumbnail() @@ -1032,7 +1033,7 @@ class ProjectPushItemProcess: copied_status = self._get_transferable_status(src_version_entity) version_entity = new_version_entity( - version, + dst_version, product_id, author=src_version_entity["author"], status=copied_status, @@ -1380,7 +1381,7 @@ class IntegrateModel: variant, comment, new_folder_name, - dst_version, + version_up, use_original_name ): """Create new item for integration. @@ -1394,7 +1395,7 @@ class IntegrateModel: variant (str): Variant name. comment (Union[str, None]): Comment. new_folder_name (Union[str, None]): New folder name. - dst_version (int): Destination version number. + version_up (bool): Should destination product be versioned up use_original_name (bool): If original product names should be used Returns: @@ -1411,7 +1412,7 @@ class IntegrateModel: variant, comment=comment, new_folder_name=new_folder_name, - dst_version=dst_version, + version_up=version_up, use_original_name=use_original_name ) process_item = ProjectPushItemProcess(self, item) From 9e6dd82c7477ab10f2c0640b467b863fbf1e705a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 31 Oct 2025 17:23:18 +0100 Subject: [PATCH 86/95] Removed unnecessary call to private method --- client/ayon_core/tools/push_to_project/ui/window.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index f382ccce64..c1308cece0 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -376,7 +376,6 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._invalidate_new_folder_name( new_folder_name, user_values["is_new_folder_name_valid"] ) - self._controller._invalidate() self._projects_combobox.refresh() def _on_first_show(self): From 87ba72eb002635d0cd1c69d8739db97a25a4150e Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:44:20 +0100 Subject: [PATCH 87/95] Typos --- client/ayon_core/plugins/publish/extract_review.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_review.py b/client/ayon_core/plugins/publish/extract_review.py index 580aa27eef..56863921c0 100644 --- a/client/ayon_core/plugins/publish/extract_review.py +++ b/client/ayon_core/plugins/publish/extract_review.py @@ -361,14 +361,14 @@ class ExtractReview(pyblish.api.InstancePlugin): if not filtered_output_defs: self.log.debug(( "Repre: {} - All output definitions were filtered" - " out by single frame filter. Skipping" + " out by single frame filter. Skipped." ).format(repre["name"])) continue # Skip if file is not set if first_input_path is None: self.log.warning(( - "Representation \"{}\" have empty files. Skipped." + "Representation \"{}\" has empty files. Skipped." ).format(repre["name"])) continue From 7648d6cc817ad993edd489a8fa8ca66f2d5a01c4 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:50:50 +0100 Subject: [PATCH 88/95] Fix passing on correct filename to the representation instead of including the `1001-1025%04d` pattern used solely for OIIO sequence conversion --- .../publish/extract_color_transcode.py | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index f80ca7e150..2aefd15fd9 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -117,8 +117,6 @@ class ExtractOIIOTranscode(publish.Extractor): repre_files_to_convert = copy.deepcopy(repre["files"]) else: repre_files_to_convert = [repre["files"]] - repre_files_to_convert = self._translate_to_sequence( - repre_files_to_convert) # Process each output definition for output_def in profile_output_defs: @@ -176,11 +174,17 @@ class ExtractOIIOTranscode(publish.Extractor): additional_command_args = (output_def["oiiotool_args"] ["additional_command_args"]) - files_to_convert = self._translate_to_sequence( - files_to_convert) - self.log.debug("Files to convert: {}".format(files_to_convert)) + sequence_files = self._translate_to_sequence(files_to_convert) + self.log.debug("Files to convert: {}".format(sequence_files)) missing_rgba_review_channels = False - for file_name in files_to_convert: + 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) @@ -238,11 +242,11 @@ class ExtractOIIOTranscode(publish.Extractor): added_review = True # If there is only 1 file outputted then convert list to - # string, cause that'll indicate that its not a sequence. + # 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 its not + # 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. @@ -292,8 +296,7 @@ class ExtractOIIOTranscode(publish.Extractor): new_repre["files"] = renamed_files def _translate_to_sequence(self, files_to_convert): - """Returns original list or list with filename formatted in single - sequence format. + """Returns original list or a clique.Collection of a sequence. Uses clique to find frame sequence, in this case it merges all frames into sequence format (FRAMESTART-FRAMEEND#) and returns it. @@ -302,32 +305,26 @@ class ExtractOIIOTranscode(publish.Extractor): Args: files_to_convert (list): list of file names Returns: - (list) of [file.1001-1010#.exr] or [fileA.exr, fileB.exr] + 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] - frames = list(collection.indexes) - if collection.holes().indexes: - return files_to_convert - - # Get the padding from the collection - # This is the number of digits used in the frame numbers - padding = collection.padding - - frame_str = "{}-{}%0{}d".format(frames[0], frames[-1], padding) - file_name = "{}{}{}".format(collection.head, frame_str, - collection.tail) - - files_to_convert = [file_name] + # 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 From 76dfbaeb68e23bebc0a263af6172ccea34b2e781 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 4 Nov 2025 09:55:18 +0100 Subject: [PATCH 89/95] Update docstring --- client/ayon_core/plugins/publish/extract_color_transcode.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/client/ayon_core/plugins/publish/extract_color_transcode.py b/client/ayon_core/plugins/publish/extract_color_transcode.py index 2aefd15fd9..1a2c85e597 100644 --- a/client/ayon_core/plugins/publish/extract_color_transcode.py +++ b/client/ayon_core/plugins/publish/extract_color_transcode.py @@ -298,9 +298,8 @@ class ExtractOIIOTranscode(publish.Extractor): def _translate_to_sequence(self, files_to_convert): """Returns original list or a clique.Collection of a sequence. - Uses clique to find frame sequence, in this case it merges all frames - into sequence format (FRAMESTART-FRAMEEND#) and returns it. - If sequence not found, it returns original list + Uses clique to find frame sequence Collection. + If sequence not found, it returns original list. Args: files_to_convert (list): list of file names From 6b6001dc42e4011da76344a1c161f3eef45f2001 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 Nov 2025 14:07:58 +0100 Subject: [PATCH 90/95] Refactored usage of Qt.CheckState --- .../tools/push_to_project/ui/window.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/client/ayon_core/tools/push_to_project/ui/window.py b/client/ayon_core/tools/push_to_project/ui/window.py index c19a756bd7..b77cca0e09 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -319,6 +319,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._folder_name_input = folder_name_input self._comment_input = comment_input self._use_original_names_checkbox = original_names_checkbox + self._library_only_checkbox = library_only_checkbox self._publish_btn = publish_btn @@ -427,17 +428,18 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._comment_input_text = text self._user_input_changed_timer.start() - def _on_library_only_change(self, state: int) -> None: + def _on_library_only_change(self) -> None: """Change toggle state, reset filter, recalculate dropdown""" - state = bool(state) - self._projects_combobox.set_standard_filter_enabled(state) + is_checked = self._library_only_checkbox.isChecked() + self._projects_combobox.set_standard_filter_enabled(is_checked) - def _on_original_names_change(self, state: int) -> None: - use_original_name = bool(state) - self._invalidate_use_original_names(use_original_name) + def _on_original_names_change(self) -> None: + is_checked = self._use_original_names_checkbox.isChecked() + self._invalidate_use_original_names(is_checked) - def _on_version_up_checkbox_change(self, state: int) -> None: - self._controller.set_version_up(bool(state)) + def _on_version_up_checkbox_change(self) -> None: + is_checked = self._version_up_checkbox.isChecked() + self._controller.set_version_up(is_checked) def _on_user_input_timer(self): folder_name_enabled = self._new_folder_name_enabled From 64f511a43b2704d5e967884baa4e59d5de7861fd Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:12:17 +0100 Subject: [PATCH 91/95] don't use set for value conversion --- client/ayon_core/lib/attribute_definitions.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/lib/attribute_definitions.py b/client/ayon_core/lib/attribute_definitions.py index cb74fea0f1..36c6429f5e 100644 --- a/client/ayon_core/lib/attribute_definitions.py +++ b/client/ayon_core/lib/attribute_definitions.py @@ -604,7 +604,11 @@ class EnumDef(AbstractAttrDef): if value is None: return copy.deepcopy(self.default) - return list(self._item_values.intersection(value)) + return [ + v + for v in value + if v in self._item_values + ] def is_value_valid(self, value: Any) -> bool: """Check if item is available in possible values.""" From 434098903979a79e3c06ab0399ce7f3c2db156f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:12:45 +0100 Subject: [PATCH 92/95] avoid 'value' variable conflicts --- client/ayon_core/pipeline/create/structures.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index b2be377b42..1729d91c62 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -245,11 +245,11 @@ class AttributeValues: def _update(self, value): changes = {} - for key, value in dict(value).items(): - if key in self._data and self._data.get(key) == value: + for key, key_value in dict(value).items(): + if key in self._data and self._data.get(key) == key_value: continue - self._data[key] = value - changes[key] = value + self._data[key] = key_value + changes[key] = key_value return changes def _pop(self, key, default): From 026eb67e918b055caa7a68ee98ccfd75c07fd563 Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:13:06 +0100 Subject: [PATCH 93/95] deepcopy value --- client/ayon_core/tools/publisher/models/create.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/tools/publisher/models/create.py b/client/ayon_core/tools/publisher/models/create.py index 5098826b8b..3f5352ae8b 100644 --- a/client/ayon_core/tools/publisher/models/create.py +++ b/client/ayon_core/tools/publisher/models/create.py @@ -1,5 +1,6 @@ import logging import re +import copy from typing import ( Union, List, @@ -1098,7 +1099,7 @@ class CreateModel: creator_attributes[key] = attr_def.default elif attr_def.is_value_valid(value): - creator_attributes[key] = value + creator_attributes[key] = copy.deepcopy(value) def _set_instances_publish_attr_values( self, instance_ids, plugin_name, key, value From 2ed1d42f35d4d95f34b4c71fef756c7c062b237a Mon Sep 17 00:00:00 2001 From: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> Date: Wed, 5 Nov 2025 16:18:59 +0100 Subject: [PATCH 94/95] add comment to converted value --- client/ayon_core/pipeline/create/structures.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_core/pipeline/create/structures.py b/client/ayon_core/pipeline/create/structures.py index 1729d91c62..fecb3a5ca4 100644 --- a/client/ayon_core/pipeline/create/structures.py +++ b/client/ayon_core/pipeline/create/structures.py @@ -137,6 +137,7 @@ class AttributeValues: if value is None: continue converted_value = attr_def.convert_value(value) + # QUESTION Could we just use converted value all the time? if converted_value == value: self._data[attr_def.key] = value From 84db5d396517c74fd9634b3e0d5a8df0938acf52 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 5 Nov 2025 23:00:22 +0100 Subject: [PATCH 95/95] Update client/ayon_core/lib/transcoding.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- client/ayon_core/lib/transcoding.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/ayon_core/lib/transcoding.py b/client/ayon_core/lib/transcoding.py index 38033d084f..22396a5324 100644 --- a/client/ayon_core/lib/transcoding.py +++ b/client/ayon_core/lib/transcoding.py @@ -441,16 +441,16 @@ def get_review_info_by_layer_name(channel_names): def _sort(_layer_name: str) -> int: # Prioritize "" layer name # Prioritize layers with RGB channels - order = 0 if _layer_name == "rgba": - order -= 11 + return 0 + if _layer_name == "": - order -= 10 + return 1 channels = channels_by_layer_name[_layer_name] if all(channel in channels for channel in "RGB"): - order -= 1 - return order + return 2 + return 10 layer_names_order.sort(key=_sort) output = []