From e86450c48df1bc78c400cfa750605192e1c48125 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 May 2025 12:57:56 +0200 Subject: [PATCH 1/9] Allow Templated Workfile Build to build from an AYON Entity URI instead of filepath or templated filepath. --- .../workfile/workfile_template_builder.py | 184 ++++++++++++------ 1 file changed, 121 insertions(+), 63 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 27da278c5e..319ebb954e 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -17,6 +17,7 @@ import collections import copy from abc import ABC, abstractmethod +import ayon_api from ayon_api import ( get_folders, get_folder_by_path, @@ -60,6 +61,28 @@ from ayon_core.pipeline.create import ( _NOT_SET = object() +def resolve_entity_uri(entity_uri: str) -> str: + """Resolve AYON entity URI to a filesystem path for local system.""" + response = ayon_api.post( + "resolve", + resolveRoots=True, + uris=[entity_uri] + ) + if response.status_code != 200: + raise RuntimeError( + f"Unable to resolve AYON entity URI filepath for " + f"'{entity_uri}': {response.text}" + ) + + entities = response.data[0]["entities"] + if len(entities) != 1: + raise RuntimeError( + f"Unable to resolve AYON entity URI '{entity_uri}' to a " + f"single filepath. Received data: {response.data}" + ) + return entities[0]["filePath"] + + class TemplateNotFound(Exception): """Exception raised when template does not exist.""" pass @@ -823,7 +846,6 @@ class AbstractTemplateBuilder(ABC): """ host_name = self.host_name - project_name = self.project_name task_name = self.current_task_name task_type = self.current_task_type @@ -836,6 +858,8 @@ class AbstractTemplateBuilder(ABC): } ) + print("Build profiles", build_profiles) + if not profile: raise TemplateProfileNotFound(( "No matching profile found for task '{}' of type '{}' " @@ -843,6 +867,15 @@ class AbstractTemplateBuilder(ABC): ).format(task_name, task_type, host_name)) path = profile["path"] + if not path: + raise TemplateLoadFailed(( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles" + ).format(host_name.title())) + + resolved_path = self.resolve_template_path(path) + self.log.info(f"Found template at: '{resolved_path}'") # switch to remove placeholders after they are used keep_placeholder = profile.get("keep_placeholder") @@ -852,86 +885,111 @@ class AbstractTemplateBuilder(ABC): if keep_placeholder is None: keep_placeholder = True - if not path: - raise TemplateLoadFailed(( - "Template path is not set.\n" - "Path need to be set in {}\\Template Workfile Build " - "Settings\\Profiles" - ).format(host_name.title())) - - # Try to fill path with environments and anatomy roots - anatomy = Anatomy(project_name) - fill_data = { - key: value - for key, value in os.environ.items() - } - - fill_data["root"] = anatomy.roots - fill_data["project"] = { - "name": project_name, - "code": anatomy.project_code, - } - - path = self.resolve_template_path(path, fill_data) - - if path and os.path.exists(path): - self.log.info("Found template at: '{}'".format(path)) - return { - "path": path, - "keep_placeholder": keep_placeholder, - "create_first_version": create_first_version - } - - solved_path = None - while True: - try: - solved_path = anatomy.path_remapper(path) - except KeyError as missing_key: - raise KeyError( - "Could not solve key '{}' in template path '{}'".format( - missing_key, path)) - - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - - solved_path = os.path.normpath(solved_path) - if not os.path.exists(solved_path): - raise TemplateNotFound( - "Template found in AYON settings for task '{}' with host " - "'{}' does not exists. (Not found : {})".format( - task_name, host_name, solved_path)) - - self.log.info("Found template at: '{}'".format(solved_path)) - return { - "path": solved_path, + "path": resolved_path, "keep_placeholder": keep_placeholder, "create_first_version": create_first_version } - def resolve_template_path(self, path, fill_data) -> str: + def resolve_template_path(self, path, fill_data=None) -> str: """Resolve the template path. - By default, this does nothing except returning the path directly. + By default, this: + - Resolves AYON entity URI to a filesystem path + - Returns path directly if it exists on disk. + - Resolves template keys through anatomy and environment variables. This can be overridden in host integrations to perform additional resolving over the template. Like, `hou.text.expandString` in Houdini. + It's recommended to still call the super().resolve_template_path() + to ensure the basic resolving is done across all integrations. Arguments: path (str): The input path. - fill_data (dict[str, str]): Data to use for template formatting. + fill_data (dict[str, str]): Deprecated. This is computed inside + the method using the current environment and project settings. + Used to be the data to use for template formatting. Returns: str: The resolved path. """ - result = StringTemplate.format_template(path, fill_data) - if result.solved: - path = result.normalized() - return path + + # If the path is an AYON entity URI, then resolve the filepath + # through the backend + if path.startswith("ayon+entity://") or path.startswith("ayon://"): + # This is a special case where the path is an AYON entity URI + # We need to resolve it to a filesystem path + resolved_path = resolve_entity_uri(path) + if not os.path.exists(resolved_path): + raise TemplateNotFound( + "Template found in AYON settings for task '{}' with host " + "'{}' does not resolve AYON entity URI '{}' " + "to an existing file on disk: '{}'".format( + self.current_task_name, + self.host_name, + path, + resolved_path, + ) + ) + return resolved_path + + # If the path is set and it's found on disk, return it directly + if path and os.path.exists(path): + return path + + # Otherwise assume a path with template keys, we do a very mundane + # check whether `{` or `<` is present in the path. + if "{" in path or "<" in path: + # Resolve keys through anatomy + project_name = self.project_name + task_name = self.current_task_name + host_name = self.host_name + + # Try to fill path with environments and anatomy roots + anatomy = Anatomy(project_name) + fill_data = { + key: value + for key, value in os.environ.items() + } + + fill_data["root"] = anatomy.roots + fill_data["project"] = { + "name": project_name, + "code": anatomy.project_code, + } + + # Recursively remap anatomy paths + while True: + try: + solved_path = anatomy.path_remapper(path) + except KeyError as missing_key: + raise KeyError( + f"Could not solve key '{missing_key}'" + f" in template path '{path}'" + ) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + + solved_path = os.path.normpath(solved_path) + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in AYON settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + result = StringTemplate.format_template(path, fill_data) + if result.solved: + path = result.normalized() + return path + + raise TemplateNotFound( + f"Unable to resolve template path: '{path}'" + ) def emit_event(self, topic, data=None, source=None) -> Event: return self._event_system.emit(topic, data, source) From 956121ba9649d1013ee36b7e1949c871e1795981 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 27 May 2025 13:01:43 +0200 Subject: [PATCH 2/9] Remove debug print --- .../ayon_core/pipeline/workfile/workfile_template_builder.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 319ebb954e..f756190991 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -857,9 +857,6 @@ class AbstractTemplateBuilder(ABC): "task_names": task_name } ) - - print("Build profiles", build_profiles) - if not profile: raise TemplateProfileNotFound(( "No matching profile found for task '{}' of type '{}' " From aec2ee828cdd3686d476b53d761f3cd152b39133 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:23:02 +0200 Subject: [PATCH 3/9] Raise dedicated `EntityResolutionError` --- .../pipeline/workfile/workfile_template_builder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index f756190991..152421f283 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -61,6 +61,10 @@ from ayon_core.pipeline.create import ( _NOT_SET = object() +class EntityResolutionError(Exception): + """Exception raised when entity URI resolution fails.""" + + def resolve_entity_uri(entity_uri: str) -> str: """Resolve AYON entity URI to a filesystem path for local system.""" response = ayon_api.post( @@ -76,7 +80,7 @@ def resolve_entity_uri(entity_uri: str) -> str: entities = response.data[0]["entities"] if len(entities) != 1: - raise RuntimeError( + raise EntityResolutionError( f"Unable to resolve AYON entity URI '{entity_uri}' to a " f"single filepath. Received data: {response.data}" ) From 7b5ff1394c1684a9a1740ece36e5d343adf853b9 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:30:48 +0200 Subject: [PATCH 4/9] Do not raise error on non-existing path from `resolve_template_path` to match backwards compatible behavior - feedback by @iLLiCiTiT --- .../workfile/workfile_template_builder.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 152421f283..74dc503cac 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -876,6 +876,12 @@ class AbstractTemplateBuilder(ABC): ).format(host_name.title())) resolved_path = self.resolve_template_path(path) + if not resolved_path: + raise TemplateNotFound( + f"Template path '{path}' does not resolve to a valid existing " + "template file on disk." + ) + self.log.info(f"Found template at: '{resolved_path}'") # switch to remove placeholders after they are used @@ -923,7 +929,7 @@ class AbstractTemplateBuilder(ABC): # We need to resolve it to a filesystem path resolved_path = resolve_entity_uri(path) if not os.path.exists(resolved_path): - raise TemplateNotFound( + self.log.warning( "Template found in AYON settings for task '{}' with host " "'{}' does not resolve AYON entity URI '{}' " "to an existing file on disk: '{}'".format( @@ -978,19 +984,22 @@ class AbstractTemplateBuilder(ABC): solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): - raise TemplateNotFound( + self.log.warning( "Template found in AYON settings for task '{}' with host " "'{}' does not exists. (Not found : {})".format( - task_name, host_name, solved_path)) + task_name, host_name, solved_path) + ) + return path result = StringTemplate.format_template(path, fill_data) if result.solved: path = result.normalized() return path - raise TemplateNotFound( + self.log.warning( f"Unable to resolve template path: '{path}'" ) + return path def emit_event(self, topic, data=None, source=None) -> Event: return self._event_system.emit(topic, data, source) From 706d72f045e179e9c633a4015719d8346a722ba0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:33:36 +0200 Subject: [PATCH 5/9] Check file existence for error check --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 74dc503cac..193fbeca2a 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -876,7 +876,7 @@ class AbstractTemplateBuilder(ABC): ).format(host_name.title())) resolved_path = self.resolve_template_path(path) - if not resolved_path: + if not resolved_path or not os.path.exists(resolved_path): raise TemplateNotFound( f"Template path '{path}' does not resolve to a valid existing " "template file on disk." From b523aa6b9f8286068268a88314377faca628d235 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:36:37 +0200 Subject: [PATCH 6/9] Move warning out of `resolve_template_path` function so that inherited function,e.g. like in ayon-houdini can continue without having tons of logs that are irrelevant if houdini specific logic could still resolve it. --- .../workfile/workfile_template_builder.py | 24 +++---------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index 193fbeca2a..ef909c9503 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -878,8 +878,9 @@ class AbstractTemplateBuilder(ABC): resolved_path = self.resolve_template_path(path) if not resolved_path or not os.path.exists(resolved_path): raise TemplateNotFound( - f"Template path '{path}' does not resolve to a valid existing " - "template file on disk." + "Template file found in AYON settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, resolved_path) ) self.log.info(f"Found template at: '{resolved_path}'") @@ -928,17 +929,6 @@ class AbstractTemplateBuilder(ABC): # This is a special case where the path is an AYON entity URI # We need to resolve it to a filesystem path resolved_path = resolve_entity_uri(path) - if not os.path.exists(resolved_path): - self.log.warning( - "Template found in AYON settings for task '{}' with host " - "'{}' does not resolve AYON entity URI '{}' " - "to an existing file on disk: '{}'".format( - self.current_task_name, - self.host_name, - path, - resolved_path, - ) - ) return resolved_path # If the path is set and it's found on disk, return it directly @@ -984,11 +974,6 @@ class AbstractTemplateBuilder(ABC): solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): - self.log.warning( - "Template found in AYON settings for task '{}' with host " - "'{}' does not exists. (Not found : {})".format( - task_name, host_name, solved_path) - ) return path result = StringTemplate.format_template(path, fill_data) @@ -996,9 +981,6 @@ class AbstractTemplateBuilder(ABC): path = result.normalized() return path - self.log.warning( - f"Unable to resolve template path: '{path}'" - ) return path def emit_event(self, topic, data=None, source=None) -> Event: From 16b56f7579b0b22f57db27e723f5d38761ee8404 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 19:37:16 +0200 Subject: [PATCH 7/9] Remove unused variables --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index ef909c9503..de2eb9ce5c 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -940,8 +940,6 @@ class AbstractTemplateBuilder(ABC): if "{" in path or "<" in path: # Resolve keys through anatomy project_name = self.project_name - task_name = self.current_task_name - host_name = self.host_name # Try to fill path with environments and anatomy roots anatomy = Anatomy(project_name) From 40d5efc9a5bf2e1ce14ff07b20fcf1b3b2f42ecf Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 11 Jun 2025 20:08:29 +0200 Subject: [PATCH 8/9] Fix typo --- client/ayon_core/pipeline/workfile/workfile_template_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index de2eb9ce5c..d62bb49227 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -8,7 +8,7 @@ targeted by task types and names. Placeholders are created using placeholder plugins which should care about logic and data of placeholder items. 'PlaceholderItem' is used to keep track -about it's progress. +about its progress. """ import os From d681c87aadbc14404511fd39996d9ac5ce2a3c06 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Thu, 12 Jun 2025 13:49:46 +0200 Subject: [PATCH 9/9] Restructure `resolve_template_path` --- .../workfile/workfile_template_builder.py | 64 ++++++++++--------- 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index d62bb49227..8cea7de86b 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -935,51 +935,53 @@ class AbstractTemplateBuilder(ABC): if path and os.path.exists(path): return path - # Otherwise assume a path with template keys, we do a very mundane - # check whether `{` or `<` is present in the path. - if "{" in path or "<" in path: - # Resolve keys through anatomy - project_name = self.project_name + # We may have path for another platform, like C:/path/to/file + # or a path with template keys, like {project[code]} or both. + # Try to fill path with environments and anatomy roots + project_name = self.project_name + anatomy = Anatomy(project_name) - # Try to fill path with environments and anatomy roots - anatomy = Anatomy(project_name) + # Simple check whether the path contains any template keys + if "{" in path: fill_data = { key: value for key, value in os.environ.items() } - fill_data["root"] = anatomy.roots fill_data["project"] = { "name": project_name, "code": anatomy.project_code, } - # Recursively remap anatomy paths - while True: - try: - solved_path = anatomy.path_remapper(path) - except KeyError as missing_key: - raise KeyError( - f"Could not solve key '{missing_key}'" - f" in template path '{path}'" - ) - - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - - solved_path = os.path.normpath(solved_path) - if not os.path.exists(solved_path): + # Format the template using local fill data + result = StringTemplate.format_template(path, fill_data) + if not result.solved: return path - result = StringTemplate.format_template(path, fill_data) - if result.solved: - path = result.normalized() - return path + path = result.normalized() + if os.path.exists(path): + return path - return path + # If the path were set in settings using a Windows path and we + # are now on a Linux system, we try to convert the solved path to + # the current platform. + while True: + try: + solved_path = anatomy.path_remapper(path) + except KeyError as missing_key: + raise KeyError( + f"Could not solve key '{missing_key}'" + f" in template path '{path}'" + ) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + + solved_path = os.path.normpath(solved_path) + return solved_path def emit_event(self, topic, data=None, source=None) -> Event: return self._event_system.emit(topic, data, source)