From 85c5fac7b9629f32ef8ec018188a6d7715568b79 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Fri, 14 Nov 2025 16:50:05 +0100 Subject: [PATCH 1/9] no need to specify hosts in plugin where only `shot` product type is defined. This is only used inside of any editorial or csv ingest --- client/ayon_core/plugins/publish/collect_hierarchy.py | 1 - 1 file changed, 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 56b48c37f6..8a7e32f4e4 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -13,7 +13,6 @@ class CollectHierarchy(pyblish.api.ContextPlugin): label = "Collect Hierarchy" order = pyblish.api.CollectorOrder - 0.076 - hosts = ["resolve", "hiero", "flame", "traypublisher"] def process(self, context): project_name = context.data["projectName"] From 6c768d3f9914752da31c4587655b4dd92e6e17f3 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Fri, 14 Nov 2025 17:24:30 +0100 Subject: [PATCH 2/9] Add CollectHierarchy publish plugin settings --- server/settings/publish_plugins.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index ee422a0acf..5f9e964fb3 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -48,6 +48,13 @@ class CollectAudioModel(BaseSettingsModel): "", title="Name of audio variant" ) +class CollectHierarchyModel(BaseSettingsModel): + _isGroup = True + ignore_shot_attributes_on_update: bool = SettingsField( + False, + title="Ignore shot attributes on update" + ) + class CollectSceneVersionModel(BaseSettingsModel): _isGroup = True @@ -1094,6 +1101,10 @@ class PublishPuginsModel(BaseSettingsModel): default_factory=CollectExplicitResolutionModel, title="Collect Explicit Resolution" ) + CollectHierarchy: CollectHierarchyModel = SettingsField( + default_factory=CollectHierarchyModel, + title="Collect Hierarchy" + ) ValidateEditorialAssetName: ValidateBaseModel = SettingsField( default_factory=ValidateBaseModel, title="Validate Editorial Asset Name" @@ -1275,6 +1286,9 @@ DEFAULT_PUBLISH_VALUES = { ], "options": [] }, + "CollectHierarchy": { + "ignore_shot_attributes_on_update": False, + }, "ValidateEditorialAssetName": { "enabled": True, "optional": False, From 82cb180058617e2a465e46bef9fe355d4296b518 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Fri, 14 Nov 2025 17:24:48 +0100 Subject: [PATCH 3/9] Add option to ignore shot attributes update Adds an option to the Collect Hierarchy plugin to ignore shot attributes when updating. --- .../plugins/publish/collect_hierarchy.py | 80 +++++++++++++------ 1 file changed, 57 insertions(+), 23 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 8a7e32f4e4..4a7328664d 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -1,7 +1,13 @@ import pyblish.api +from ayon_core.lib import BoolDef + +from ayon_core.pipeline import publish -class CollectHierarchy(pyblish.api.ContextPlugin): +class CollectHierarchy( + pyblish.api.ContextPlugin, + publish.AYONPyblishPluginMixin, +): """Collecting hierarchy from `parents`. present in `clip` family instances coming from the request json data file @@ -13,8 +19,35 @@ class CollectHierarchy(pyblish.api.ContextPlugin): label = "Collect Hierarchy" order = pyblish.api.CollectorOrder - 0.076 + settings_category = "core" + + ignore_shot_attributes_on_update = False + + @classmethod + def get_attr_defs_for_context(cls, create_context): + return [ + BoolDef( + "ignore_shot_attributes_on_update", + label="Ignore shot attributes on update", + default=cls.ignore_shot_attributes_on_update + ) + ] + + @classmethod + def apply_settings(cls, project_settings): + cls.ignore_shot_attributes_on_update = ( + project_settings + ["core"] + ["CollectHierarchy"] + ["ignore_shot_attributes_on_update"] + ) + def process(self, context): + values = self.get_attr_values_from_data(context.data) + ignore_shot_attributes_on_update = values.get( + "ignore_shot_attributes_on_update", None) + project_name = context.data["projectName"] final_context = { project_name: { @@ -50,29 +83,30 @@ class CollectHierarchy(pyblish.api.ContextPlugin): } shot_data["attributes"] = {} - SHOT_ATTRS = ( - "handleStart", - "handleEnd", - "frameStart", - "frameEnd", - "clipIn", - "clipOut", - "fps", - "resolutionWidth", - "resolutionHeight", - "pixelAspect", - ) - for shot_attr in SHOT_ATTRS: - attr_value = instance.data.get(shot_attr) - if attr_value is None: - # Shot attribute might not be defined (e.g. CSV ingest) - self.log.debug( - "%s shot attribute is not defined for instance.", - shot_attr - ) - continue + if not ignore_shot_attributes_on_update: + SHOT_ATTRS = ( + "handleStart", + "handleEnd", + "frameStart", + "frameEnd", + "clipIn", + "clipOut", + "fps", + "resolutionWidth", + "resolutionHeight", + "pixelAspect", + ) + for shot_attr in SHOT_ATTRS: + attr_value = instance.data.get(shot_attr) + if attr_value is None: + # Shot attribute might not be defined (e.g. CSV ingest) + self.log.debug( + "%s shot attribute is not defined for instance.", + shot_attr + ) + continue - shot_data["attributes"][shot_attr] = attr_value + shot_data["attributes"][shot_attr] = attr_value # Split by '/' for AYON where asset is a path name = instance.data["folderPath"].split("/")[-1] From 39e12331f0d9be7083c46378e20a9f852e6a9803 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Fri, 14 Nov 2025 17:27:10 +0100 Subject: [PATCH 4/9] adding todo --- client/ayon_core/plugins/publish/collect_hierarchy.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 4a7328664d..a52230d751 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -83,6 +83,9 @@ class CollectHierarchy( } shot_data["attributes"] = {} + # TODO(jakubjezek001): we need to check if the shot already exists + # and if not the attributes needs to be added in case the option + # is disabled by settings if not ignore_shot_attributes_on_update: SHOT_ATTRS = ( "handleStart", From 8fcd6c042578b659181ad4d0b38cebf76cb9e5fe Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Fri, 14 Nov 2025 17:54:31 +0100 Subject: [PATCH 5/9] Collect existing folder entities for shots Collect existing folder entities for shots to check if shot attributes should be synced during update. --- .../plugins/publish/collect_hierarchy.py | 96 +++++++++++++++---- 1 file changed, 76 insertions(+), 20 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index a52230d751..81188ace6a 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -1,6 +1,6 @@ import pyblish.api from ayon_core.lib import BoolDef - +import ayon_api from ayon_core.pipeline import publish @@ -42,23 +42,17 @@ class CollectHierarchy( ["ignore_shot_attributes_on_update"] ) + def _get_shot_instances(self, context): + """Get shot instances from context. - def process(self, context): - values = self.get_attr_values_from_data(context.data) - ignore_shot_attributes_on_update = values.get( - "ignore_shot_attributes_on_update", None) + Args: + context (pyblish.api.Context): Context is a list of instances. - project_name = context.data["projectName"] - final_context = { - project_name: { - "entity_type": "project", - "children": {} - }, - } - temp_context = {} + Returns: + list[pyblish.api.Instance]: A list of shot instances. + """ + shot_instances = [] for instance in context: - self.log.debug("Processing instance: `{}` ...".format(instance)) - # shot data dict product_type = instance.data["productType"] families = instance.data["families"] @@ -73,6 +67,65 @@ class CollectHierarchy( self.log.debug("Skipping not a shot from hero track") continue + shot_instances.append(instance) + + return shot_instances + + def get_existing_folder_entities(self, project_name, shot_instances): + """Get existing folder entities for given shot instances. + + Args: + project_name (str): The name of the project. + shot_instances (list[pyblish.api.Instance]): A list of shot + instances. + + Returns: + dict[str, dict]: A dictionary mapping folder paths to existing + folder entities. + """ + # first we need to get all folder paths from shot instances + folder_paths = [] + for instance in shot_instances: + folder_path = instance.data["folderPath"] + folder_paths.append(folder_path) + + # then we get all existing folder entities with one request + existing_entities = ayon_api.get_folders( + project_name, folder_paths=folder_paths) + + # then we loop by all folder paths and try to find existing entity + existing_entities = {} + for folder_path in folder_paths: + found_entity = None + for entity in existing_entities: + if entity["path"] == folder_path: + found_entity = entity + break + existing_entities[folder_path] = found_entity + + return existing_entities + + def process(self, context): + values = self.get_attr_values_from_data(context.data) + ignore_shot_attributes_on_update = values.get( + "ignore_shot_attributes_on_update", None) + + project_name = context.data["projectName"] + final_context = { + project_name: { + "entity_type": "project", + "children": {} + }, + } + temp_context = {} + shot_instances = self._get_shot_instances(context) + existing_entities = self.get_existing_folder_entities( + project_name, shot_instances) + + for instance in shot_instances: + self.log.debug("Processing instance: `{}` ...".format(instance)) + folder_path = instance.data["folderPath"] + shot_data = { "entity_type": "folder", # WARNING unless overwritten, default folder type is hardcoded @@ -83,10 +136,13 @@ class CollectHierarchy( } shot_data["attributes"] = {} - # TODO(jakubjezek001): we need to check if the shot already exists - # and if not the attributes needs to be added in case the option - # is disabled by settings - if not ignore_shot_attributes_on_update: + # we need to check if the shot entity already exists + # and if not the attributes needs to be added in case the option + # is disabled by settings + if ( + existing_entities.get(folder_path) + and not ignore_shot_attributes_on_update + ): SHOT_ATTRS = ( "handleStart", "handleEnd", @@ -112,7 +168,7 @@ class CollectHierarchy( shot_data["attributes"][shot_attr] = attr_value # Split by '/' for AYON where asset is a path - name = instance.data["folderPath"].split("/")[-1] + name = folder_path.split("/")[-1] actual = {name: shot_data} for parent in reversed(instance.data["parents"]): From ea598a301aec079047986a177b2c4fdb65fb3821 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Fri, 14 Nov 2025 18:09:08 +0100 Subject: [PATCH 6/9] Add missing blank line in publish_plugins.py --- server/settings/publish_plugins.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py index 5f9e964fb3..ae660b75fc 100644 --- a/server/settings/publish_plugins.py +++ b/server/settings/publish_plugins.py @@ -48,6 +48,7 @@ class CollectAudioModel(BaseSettingsModel): "", title="Name of audio variant" ) + class CollectHierarchyModel(BaseSettingsModel): _isGroup = True ignore_shot_attributes_on_update: bool = SettingsField( From 4d47cb78b39b65e99bd273c05ab15059675b142c Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Tue, 18 Nov 2025 13:28:22 +0100 Subject: [PATCH 7/9] Refactor: Simplify CollectHierarchy logic Simplify folder path collection and existing entity lookup for CollectHierarchy plugin. Use sets and dicts for improved efficiency and readability. --- .../plugins/publish/collect_hierarchy.py | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 81188ace6a..145c2beb9b 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -84,28 +84,30 @@ class CollectHierarchy( folder entities. """ # first we need to get all folder paths from shot instances - folder_paths = [] - for instance in shot_instances: - folder_path = instance.data["folderPath"] - folder_paths.append(folder_path) - + folder_paths = { + instance.data["folderPath"] + for instance in shot_instances + } # then we get all existing folder entities with one request - existing_entities = ayon_api.get_folders( - project_name, folder_paths=folder_paths) - - # then we loop by all folder paths and try to find existing entity - existing_entities = {} + existing_entities = { + folder_entity["path"]: folder_entity + for folder_entity in ayon_api.get_folders( + project_name, folder_paths=folder_paths) + } for folder_path in folder_paths: - found_entity = None - for entity in existing_entities: - if entity["path"] == folder_path: - found_entity = entity - break - existing_entities[folder_path] = found_entity + # add None value to non-existing folder entities + existing_entities.setdefault(folder_path, None) return existing_entities def process(self, context): + # get only shot instances from context + shot_instances = self._get_shot_instances(context) + + if not shot_instances: + return + + # get user input values = self.get_attr_values_from_data(context.data) ignore_shot_attributes_on_update = values.get( "ignore_shot_attributes_on_update", None) @@ -118,7 +120,6 @@ class CollectHierarchy( }, } temp_context = {} - shot_instances = self._get_shot_instances(context) existing_entities = self.get_existing_folder_entities( project_name, shot_instances) From ec1e1183463a9c5153718fcaea06c38d80ca0877 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Tue, 18 Nov 2025 13:33:32 +0100 Subject: [PATCH 8/9] Collect only 'path' field when checking hierarchy The collect hierarchy plugin was retrieving all fields when getting existing folders from the database, but only the 'path' field is needed. This change reduces the amount of data transferred. --- client/ayon_core/plugins/publish/collect_hierarchy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 145c2beb9b..7c2b2fd5d1 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -92,7 +92,7 @@ class CollectHierarchy( existing_entities = { folder_entity["path"]: folder_entity for folder_entity in ayon_api.get_folders( - project_name, folder_paths=folder_paths) + project_name, folder_paths=folder_paths, fields={"path"}) } for folder_path in folder_paths: # add None value to non-existing folder entities From 94a8ddbd0f80c199f443297076405f9bbd607367 Mon Sep 17 00:00:00 2001 From: jakubjezek001 Date: Tue, 18 Nov 2025 14:03:57 +0100 Subject: [PATCH 9/9] Debug log instance folder path and attributes update state --- client/ayon_core/plugins/publish/collect_hierarchy.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 7c2b2fd5d1..1f4900359f 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -124,8 +124,9 @@ class CollectHierarchy( project_name, shot_instances) for instance in shot_instances: - self.log.debug("Processing instance: `{}` ...".format(instance)) folder_path = instance.data["folderPath"] + self.log.debug( + f"Processing instance: `{folder_path} {instance}` ...") shot_data = { "entity_type": "folder", @@ -167,6 +168,10 @@ class CollectHierarchy( continue shot_data["attributes"][shot_attr] = attr_value + else: + self.log.debug( + "Shot attributes will not be updated." + ) # Split by '/' for AYON where asset is a path name = folder_path.split("/")[-1]