mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-01 16:34:53 +01:00
Merge pull request #1292 from BigRoy/enhancement/workfile_template_builder_allow_published_entities
Templated Workfile Build: support building from an AYON Entity URI instead of filepath
This commit is contained in:
commit
15b4f236f6
1 changed files with 113 additions and 63 deletions
|
|
@ -8,7 +8,7 @@ targeted by task types and names.
|
||||||
|
|
||||||
Placeholders are created using placeholder plugins which should care about
|
Placeholders are created using placeholder plugins which should care about
|
||||||
logic and data of placeholder items. 'PlaceholderItem' is used to keep track
|
logic and data of placeholder items. 'PlaceholderItem' is used to keep track
|
||||||
about it's progress.
|
about its progress.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
@ -17,6 +17,7 @@ import collections
|
||||||
import copy
|
import copy
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
|
||||||
|
import ayon_api
|
||||||
from ayon_api import (
|
from ayon_api import (
|
||||||
get_folders,
|
get_folders,
|
||||||
get_folder_by_path,
|
get_folder_by_path,
|
||||||
|
|
@ -60,6 +61,32 @@ from ayon_core.pipeline.create import (
|
||||||
_NOT_SET = object()
|
_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(
|
||||||
|
"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 EntityResolutionError(
|
||||||
|
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):
|
class TemplateNotFound(Exception):
|
||||||
"""Exception raised when template does not exist."""
|
"""Exception raised when template does not exist."""
|
||||||
pass
|
pass
|
||||||
|
|
@ -823,7 +850,6 @@ class AbstractTemplateBuilder(ABC):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
host_name = self.host_name
|
host_name = self.host_name
|
||||||
project_name = self.project_name
|
|
||||||
task_name = self.current_task_name
|
task_name = self.current_task_name
|
||||||
task_type = self.current_task_type
|
task_type = self.current_task_type
|
||||||
|
|
||||||
|
|
@ -835,7 +861,6 @@ class AbstractTemplateBuilder(ABC):
|
||||||
"task_names": task_name
|
"task_names": task_name
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not profile:
|
if not profile:
|
||||||
raise TemplateProfileNotFound((
|
raise TemplateProfileNotFound((
|
||||||
"No matching profile found for task '{}' of type '{}' "
|
"No matching profile found for task '{}' of type '{}' "
|
||||||
|
|
@ -843,6 +868,22 @@ class AbstractTemplateBuilder(ABC):
|
||||||
).format(task_name, task_type, host_name))
|
).format(task_name, task_type, host_name))
|
||||||
|
|
||||||
path = profile["path"]
|
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)
|
||||||
|
if not resolved_path or not os.path.exists(resolved_path):
|
||||||
|
raise TemplateNotFound(
|
||||||
|
"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}'")
|
||||||
|
|
||||||
# switch to remove placeholders after they are used
|
# switch to remove placeholders after they are used
|
||||||
keep_placeholder = profile.get("keep_placeholder")
|
keep_placeholder = profile.get("keep_placeholder")
|
||||||
|
|
@ -852,44 +893,86 @@ class AbstractTemplateBuilder(ABC):
|
||||||
if keep_placeholder is None:
|
if keep_placeholder is None:
|
||||||
keep_placeholder = True
|
keep_placeholder = True
|
||||||
|
|
||||||
if not path:
|
return {
|
||||||
raise TemplateLoadFailed((
|
"path": resolved_path,
|
||||||
"Template path is not set.\n"
|
"keep_placeholder": keep_placeholder,
|
||||||
"Path need to be set in {}\\Template Workfile Build "
|
"create_first_version": create_first_version
|
||||||
"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
|
def resolve_template_path(self, path, fill_data=None) -> str:
|
||||||
fill_data["project"] = {
|
"""Resolve the template path.
|
||||||
"name": project_name,
|
|
||||||
"code": anatomy.project_code,
|
|
||||||
}
|
|
||||||
|
|
||||||
path = self.resolve_template_path(path, fill_data)
|
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]): 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
return resolved_path
|
||||||
|
|
||||||
|
# If the path is set and it's found on disk, return it directly
|
||||||
if path and os.path.exists(path):
|
if path and os.path.exists(path):
|
||||||
self.log.info("Found template at: '{}'".format(path))
|
return path
|
||||||
return {
|
|
||||||
"path": path,
|
# We may have path for another platform, like C:/path/to/file
|
||||||
"keep_placeholder": keep_placeholder,
|
# or a path with template keys, like {project[code]} or both.
|
||||||
"create_first_version": create_first_version
|
# Try to fill path with environments and anatomy roots
|
||||||
|
project_name = self.project_name
|
||||||
|
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,
|
||||||
}
|
}
|
||||||
|
|
||||||
solved_path = None
|
# Format the template using local fill data
|
||||||
|
result = StringTemplate.format_template(path, fill_data)
|
||||||
|
if not result.solved:
|
||||||
|
return path
|
||||||
|
|
||||||
|
path = result.normalized()
|
||||||
|
if os.path.exists(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:
|
while True:
|
||||||
try:
|
try:
|
||||||
solved_path = anatomy.path_remapper(path)
|
solved_path = anatomy.path_remapper(path)
|
||||||
except KeyError as missing_key:
|
except KeyError as missing_key:
|
||||||
raise KeyError(
|
raise KeyError(
|
||||||
"Could not solve key '{}' in template path '{}'".format(
|
f"Could not solve key '{missing_key}'"
|
||||||
missing_key, path))
|
f" in template path '{path}'"
|
||||||
|
)
|
||||||
|
|
||||||
if solved_path is None:
|
if solved_path is None:
|
||||||
solved_path = path
|
solved_path = path
|
||||||
|
|
@ -898,40 +981,7 @@ class AbstractTemplateBuilder(ABC):
|
||||||
path = solved_path
|
path = solved_path
|
||||||
|
|
||||||
solved_path = os.path.normpath(solved_path)
|
solved_path = os.path.normpath(solved_path)
|
||||||
if not os.path.exists(solved_path):
|
return 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,
|
|
||||||
"keep_placeholder": keep_placeholder,
|
|
||||||
"create_first_version": create_first_version
|
|
||||||
}
|
|
||||||
|
|
||||||
def resolve_template_path(self, path, fill_data) -> str:
|
|
||||||
"""Resolve the template path.
|
|
||||||
|
|
||||||
By default, this does nothing except returning the path directly.
|
|
||||||
|
|
||||||
This can be overridden in host integrations to perform additional
|
|
||||||
resolving over the template. Like, `hou.text.expandString` in Houdini.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
path (str): The input path.
|
|
||||||
fill_data (dict[str, str]): 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
|
|
||||||
|
|
||||||
def emit_event(self, topic, data=None, source=None) -> Event:
|
def emit_event(self, topic, data=None, source=None) -> Event:
|
||||||
return self._event_system.emit(topic, data, source)
|
return self._event_system.emit(topic, data, source)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue