Skip to content

Commit

Permalink
Merge pull request #295 from BigRoy/enhancement/usd_workflow
Browse files Browse the repository at this point in the history
Houdini: Implement USD workflow with global asset/shot contributions plug-in
  • Loading branch information
antirotor authored Jul 2, 2024
2 parents 3528bc3 + 31cac8a commit 2e73262
Show file tree
Hide file tree
Showing 56 changed files with 4,363 additions and 1,362 deletions.
91 changes: 91 additions & 0 deletions client/ayon_core/pipeline/entity_uri.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
from typing import Optional, Union
from urllib.parse import urlparse, parse_qs


def parse_ayon_entity_uri(uri: str) -> Optional[dict]:
"""Parse AYON entity URI into individual components.
URI specification:
ayon+entity://{project}/{folder}?product={product}
&version={version}
&representation={representation}
URI example:
ayon+entity://test/hero?product=modelMain&version=2&representation=usd
However - if the netloc is `ayon://` it will by default also resolve as
`ayon+entity://` on AYON server, thus we need to support both. The shorter
`ayon://` is preferred for user readability.
Example:
>>> parse_ayon_entity_uri(
>>> "ayon://test/char/villain?product=modelMain&version=2&representation=usd" # noqa: E501
>>> )
{'project': 'test', 'folderPath': '/char/villain',
'product': 'modelMain', 'version': 1,
'representation': 'usd'}
>>> parse_ayon_entity_uri(
>>> "ayon+entity://project/folder?product=renderMain&version=3&representation=exr" # noqa: E501
>>> )
{'project': 'project', 'folderPath': '/folder',
'product': 'renderMain', 'version': 3,
'representation': 'exr'}
Returns:
dict[str, Union[str, int]]: The individual key with their values as
found in the ayon entity URI.
"""

if not (uri.startswith("ayon+entity://") or uri.startswith("ayon://")):
return {}

parsed = urlparse(uri)
if parsed.scheme not in {"ayon+entity", "ayon"}:
return {}

result = {
"project": parsed.netloc,
"folderPath": "/" + parsed.path.strip("/")
}
query = parse_qs(parsed.query)
for key in ["product", "version", "representation"]:
if key in query:
result[key] = query[key][0]

# Convert version to integer if it is a digit
version = result.get("version")
if version is not None and version.isdigit():
result["version"] = int(version)

return result


def construct_ayon_entity_uri(
project_name: str,
folder_path: str,
product: str,
version: Union[int, str],
representation_name: str
) -> str:
"""Construct AYON entity URI from its components
Returns:
str: AYON Entity URI to query entity path.
"""
if isinstance(version, int) and version < 0:
version = "hero"
if not (isinstance(version, int) or version in {"latest", "hero"}):
raise ValueError(
"Version must either be integer, 'latest' or 'hero'. "
"Got: {}".format(version)
)
return (
"ayon://{project}/{folder_path}?product={product}&version={version}"
"&representation={representation}".format(
project=project_name,
folder_path=folder_path,
product=product,
version=version,
representation=representation_name
)
)
87 changes: 86 additions & 1 deletion client/ayon_core/pipeline/load/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import inspect
import collections
import numbers
from typing import Any
from typing import Optional, Union, Any

import ayon_api

Expand Down Expand Up @@ -739,6 +739,91 @@ def path_from_data():
)


def get_representation_path_by_names(
project_name: str,
folder_path: str,
product_name: str,
version_name: str,
representation_name: str,
anatomy: Optional[Anatomy] = None) -> Optional[str]:
"""Get (latest) filepath for representation for folder and product.
See `get_representation_by_names` for more details.
Returns:
str: The representation path if the representation exists.
"""
representation = get_representation_by_names(
project_name,
folder_path,
product_name,
version_name,
representation_name
)
if not representation:
return

if not anatomy:
anatomy = Anatomy(project_name)

if representation:
path = get_representation_path_with_anatomy(representation, anatomy)
return str(path).replace("\\", "/")


def get_representation_by_names(
project_name: str,
folder_path: str,
product_name: str,
version_name: Union[int, str],
representation_name: str,
) -> Optional[dict]:
"""Get representation entity for asset and subset.
If version_name is "hero" then return the hero version
If version_name is "latest" then return the latest version
Otherwise use version_name as the exact integer version name.
"""

if isinstance(folder_path, dict) and "name" in folder_path:
# Allow explicitly passing asset document
folder_entity = folder_path
else:
folder_entity = ayon_api.get_folder_by_path(
project_name, folder_path, fields=["id"])
if not folder_entity:
return

if isinstance(product_name, dict) and "name" in product_name:
# Allow explicitly passing subset document
product_entity = product_name
else:
product_entity = ayon_api.get_product_by_name(
project_name,
product_name,
folder_id=folder_entity["id"],
fields=["id"])
if not product_entity:
return

if version_name == "hero":
version_entity = ayon_api.get_hero_version_by_product_id(
project_name, product_id=product_entity["id"])
elif version_name == "latest":
version_entity = ayon_api.get_last_version_by_product_id(
project_name, product_id=product_entity["id"])
else:
version_entity = ayon_api.get_version_by_name(
project_name, version_name, product_id=product_entity["id"])
if not version_entity:
return

return ayon_api.get_representation_by_name(
project_name, representation_name, version_id=version_entity["id"])


def is_compatible_loader(Loader, context):
"""Return whether a loader is compatible with a context.
Expand Down
47 changes: 46 additions & 1 deletion client/ayon_core/pipeline/publish/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import copy
import tempfile
import xml.etree.ElementTree
from typing import Optional, Union

import pyblish.util
import pyblish.plugin
Expand All @@ -20,7 +21,6 @@
Anatomy
)
from ayon_core.pipeline.plugin_discover import DiscoverResult

from .constants import (
DEFAULT_PUBLISH_TEMPLATE,
DEFAULT_HERO_PUBLISH_TEMPLATE,
Expand Down Expand Up @@ -933,3 +933,48 @@ def get_publish_instance_families(instance):
families.discard(family)
output.extend(families)
return output


def get_instance_expected_output_path(
instance: pyblish.api.Instance,
representation_name: str,
ext: Union[str, None],
version: Optional[str] = None
):
"""Return expected publish filepath for representation in instance
This does not validate whether the instance has any representation by the
given name, extension and/or version.
Arguments:
instance (pyblish.api.Instance): Publish instance
representation_name (str): Representation name
ext (Union[str, None]): Extension for the file.
When None, the `ext` will be set to the representation name.
version (Optional[int]): If provided, force it to format to this
particular version.
Returns:
str: Resolved path
"""

if ext is None:
ext = representation_name
if version is None:
version = instance.data["version"]

context = instance.context
anatomy = context.data["anatomy"]

template_data = copy.deepcopy(instance.data["anatomyData"])
template_data.update({
"ext": ext,
"representation": representation_name,
"variant": instance.data.get("variant"),
"version": version
})

path_template_obj = anatomy.get_template_item("publish", "default")["path"]
template_filled = path_template_obj.format_strict(template_data)
return os.path.normpath(template_filled)
Loading

0 comments on commit 2e73262

Please sign in to comment.