diff --git a/client/ayon_core/__init__.py b/client/ayon_core/__init__.py index 5f9eb6cea3..7d95587e8a 100644 --- a/client/ayon_core/__init__.py +++ b/client/ayon_core/__init__.py @@ -1,12 +1,16 @@ import os +from .version import __version__ AYON_CORE_ROOT = os.path.dirname(os.path.abspath(__file__)) -# TODO remove after '1.x.x' +# ------------------------- +# DEPRECATED - Remove before '1.x.x' release +# ------------------------- PACKAGE_DIR = AYON_CORE_ROOT PLUGINS_DIR = os.path.join(AYON_CORE_ROOT, "plugins") AYON_SERVER_ENABLED = True # Indicate if AYON entities should be used instead of OpenPype entities -USE_AYON_ENTITIES = False +USE_AYON_ENTITIES = True +# ------------------------- diff --git a/client/ayon_core/addon/base.py b/client/ayon_core/addon/base.py index f0763649ca..bbd5a486fe 100644 --- a/client/ayon_core/addon/base.py +++ b/client/ayon_core/addon/base.py @@ -14,9 +14,9 @@ from abc import ABCMeta, abstractmethod import six import appdirs +import ayon_api from ayon_core.lib import Logger, is_dev_mode_enabled -from ayon_core.client import get_ayon_server_api_connection from ayon_core.settings import get_studio_settings from .interfaces import ( @@ -147,8 +147,7 @@ def load_addons(force=False): def _get_ayon_bundle_data(): - con = get_ayon_server_api_connection() - bundles = con.get_bundles()["bundles"] + bundles = ayon_api.get_bundles()["bundles"] bundle_name = os.getenv("AYON_BUNDLE_NAME") @@ -176,8 +175,7 @@ def _get_ayon_addons_information(bundle_info): output = [] bundle_addons = bundle_info["addons"] - con = get_ayon_server_api_connection() - addons = con.get_addons_info()["addons"] + addons = ayon_api.get_addons_info()["addons"] for addon in addons: name = addon["name"] versions = addon.get("versions") diff --git a/client/ayon_core/cli.py b/client/ayon_core/cli.py index 88b574da76..0ad4364bcd 100644 --- a/client/ayon_core/cli.py +++ b/client/ayon_core/cli.py @@ -11,6 +11,7 @@ import acre from ayon_core import AYON_CORE_ROOT from ayon_core.addon import AddonsManager from ayon_core.settings import get_general_environments +from ayon_core.lib import initialize_ayon_connection from .cli_commands import Commands @@ -243,6 +244,7 @@ def _set_addons_environments(): def main(*args, **kwargs): + initialize_ayon_connection() python_path = os.getenv("PYTHONPATH", "") split_paths = python_path.split(os.pathsep) diff --git a/client/ayon_core/cli_commands.py b/client/ayon_core/cli_commands.py index a24710aef2..156196c401 100644 --- a/client/ayon_core/cli_commands.py +++ b/client/ayon_core/cli_commands.py @@ -181,7 +181,7 @@ class Commands: json.dump(env, file_stream, indent=4) @staticmethod - def contextselection(output_path, project_name, asset_name, strict): + def contextselection(output_path, project_name, folder_path, strict): from ayon_core.tools.context_dialog import main - main(output_path, project_name, asset_name, strict) + main(output_path, project_name, folder_path, strict) diff --git a/client/ayon_core/client/__init__.py b/client/ayon_core/client/__init__.py deleted file mode 100644 index 00f4d9863f..0000000000 --- a/client/ayon_core/client/__init__.py +++ /dev/null @@ -1,110 +0,0 @@ -from .utils import get_ayon_server_api_connection - -from .entities import ( - get_projects, - get_project, - get_whole_project, - - get_asset_by_id, - get_asset_by_name, - get_assets, - get_archived_assets, - get_asset_ids_with_subsets, - - get_subset_by_id, - get_subset_by_name, - get_subsets, - get_subset_families, - - get_version_by_id, - get_version_by_name, - get_versions, - get_hero_version_by_id, - get_hero_version_by_subset_id, - get_hero_versions, - get_last_versions, - get_last_version_by_subset_id, - get_last_version_by_subset_name, - get_output_link_versions, - - version_is_latest, - - get_representation_by_id, - get_representation_by_name, - get_representations, - get_representation_parents, - get_representations_parents, - get_archived_representations, - - get_thumbnail, - get_thumbnails, - get_thumbnail_id_from_source, - - get_workfile_info, - - get_asset_name_identifier, -) - -from .entity_links import ( - get_linked_asset_ids, - get_linked_assets, - get_linked_representation_id, -) - -from .operations import ( - create_project, -) - - -__all__ = ( - "get_ayon_server_api_connection", - - "get_projects", - "get_project", - "get_whole_project", - - "get_asset_by_id", - "get_asset_by_name", - "get_assets", - "get_archived_assets", - "get_asset_ids_with_subsets", - - "get_subset_by_id", - "get_subset_by_name", - "get_subsets", - "get_subset_families", - - "get_version_by_id", - "get_version_by_name", - "get_versions", - "get_hero_version_by_id", - "get_hero_version_by_subset_id", - "get_hero_versions", - "get_last_versions", - "get_last_version_by_subset_id", - "get_last_version_by_subset_name", - "get_output_link_versions", - - "version_is_latest", - - "get_representation_by_id", - "get_representation_by_name", - "get_representations", - "get_representation_parents", - "get_representations_parents", - "get_archived_representations", - - "get_thumbnail", - "get_thumbnails", - "get_thumbnail_id_from_source", - - "get_workfile_info", - - "get_linked_asset_ids", - "get_linked_assets", - "get_linked_representation_id", - - "create_project", - - "get_asset_name_identifier", -) diff --git a/client/ayon_core/client/constants.py b/client/ayon_core/client/constants.py deleted file mode 100644 index 379c0d665f..0000000000 --- a/client/ayon_core/client/constants.py +++ /dev/null @@ -1,28 +0,0 @@ -# --- Folders --- -DEFAULT_FOLDER_FIELDS = { - "id", - "name", - "path", - "parentId", - "active", - "parents", - "thumbnailId" -} - -REPRESENTATION_FILES_FIELDS = { - "files.name", - "files.hash", - "files.id", - "files.path", - "files.size", -} - -CURRENT_PROJECT_SCHEMA = "openpype:project-3.0" -CURRENT_PROJECT_CONFIG_SCHEMA = "openpype:config-2.0" -CURRENT_ASSET_DOC_SCHEMA = "openpype:asset-3.0" -CURRENT_SUBSET_SCHEMA = "openpype:subset-3.0" -CURRENT_VERSION_SCHEMA = "openpype:version-3.0" -CURRENT_HERO_VERSION_SCHEMA = "openpype:hero_version-1.0" -CURRENT_REPRESENTATION_SCHEMA = "openpype:representation-2.0" -CURRENT_WORKFILE_INFO_SCHEMA = "openpype:workfile-1.0" -CURRENT_THUMBNAIL_SCHEMA = "openpype:thumbnail-1.0" diff --git a/client/ayon_core/client/conversion_utils.py b/client/ayon_core/client/conversion_utils.py deleted file mode 100644 index 192eb194db..0000000000 --- a/client/ayon_core/client/conversion_utils.py +++ /dev/null @@ -1,1362 +0,0 @@ -import os -import arrow -import collections -import json - -import six - -from ayon_core.client.operations_base import REMOVED_VALUE -from .constants import ( - CURRENT_PROJECT_SCHEMA, - CURRENT_ASSET_DOC_SCHEMA, - CURRENT_SUBSET_SCHEMA, - CURRENT_VERSION_SCHEMA, - CURRENT_HERO_VERSION_SCHEMA, - CURRENT_REPRESENTATION_SCHEMA, - CURRENT_WORKFILE_INFO_SCHEMA, - REPRESENTATION_FILES_FIELDS, -) -from .utils import create_entity_id, prepare_entity_changes - -# --- Project entity --- -PROJECT_FIELDS_MAPPING_V3_V4 = { - "_id": {"name"}, - "name": {"name"}, - "data": {"data", "code"}, - "data.library_project": {"library"}, - "data.code": {"code"}, - "data.active": {"active"}, -} - -# TODO this should not be hardcoded but received from server!!! -# --- Folder entity --- -FOLDER_FIELDS_MAPPING_V3_V4 = { - "_id": {"id"}, - "name": {"name"}, - "label": {"label"}, - "data": { - "parentId", "parents", "active", "tasks", "thumbnailId" - }, - "data.visualParent": {"parentId"}, - "data.parents": {"parents"}, - "data.active": {"active"}, - "data.thumbnail_id": {"thumbnailId"}, - "data.entityType": {"folderType"} -} - -# --- Subset entity --- -SUBSET_FIELDS_MAPPING_V3_V4 = { - "_id": {"id"}, - "name": {"name"}, - "data.active": {"active"}, - "parent": {"folderId"} -} - -# --- Version entity --- -VERSION_FIELDS_MAPPING_V3_V4 = { - "_id": {"id"}, - "name": {"version"}, - "parent": {"productId"} -} - -# --- Representation entity --- -REPRESENTATION_FIELDS_MAPPING_V3_V4 = { - "_id": {"id"}, - "name": {"name"}, - "parent": {"versionId"}, - "context": {"context"}, - "files": {"files"}, -} - - -def project_fields_v3_to_v4(fields, con): - """Convert project fields from v3 to v4 structure. - - Args: - fields (Union[Iterable(str), None]): fields to be converted. - - Returns: - Union[Set(str), None]: Converted fields to v4 fields. - """ - - # TODO config fields - # - config.apps - # - config.groups - if not fields: - return None - - project_attribs = con.get_attributes_for_type("project") - output = set() - for field in fields: - # If config is needed the rest api call must be used - if field.startswith("config"): - return None - - if field in PROJECT_FIELDS_MAPPING_V3_V4: - output |= PROJECT_FIELDS_MAPPING_V3_V4[field] - if field == "data": - output |= { - "attrib.{}".format(attr) - for attr in project_attribs - } - - elif field.startswith("data"): - field_parts = field.split(".") - field_parts.pop(0) - data_key = ".".join(field_parts) - if data_key in project_attribs: - output.add("attrib.{}".format(data_key)) - else: - output.add("data") - print("Requested specific key from data {}".format(data_key)) - - else: - raise ValueError("Unknown field mapping for {}".format(field)) - - if "name" not in output: - output.add("name") - return output - - -def _get_default_template_name(templates): - default_template = None - for name, template in templates.items(): - if name == "default": - return "default" - - if default_template is None: - default_template = name - - return default_template - - -def _template_replacements_to_v3(template): - return ( - template - .replace("{product[name]}", "{subset}") - .replace("{product[type]}", "{family}") - ) - - -def _convert_template_item(template_item): - for key, value in tuple(template_item.items()): - template_item[key] = _template_replacements_to_v3(value) - - # Change 'directory' to 'folder' - if "directory" in template_item: - template_item["folder"] = template_item.pop("directory") - - if ( - "path" not in template_item - and "file" in template_item - and "folder" in template_item - ): - template_item["path"] = "/".join( - (template_item["folder"], template_item["file"]) - ) - - -def _fill_template_category(templates, cat_templates, cat_key): - default_template_name = _get_default_template_name(cat_templates) - for template_name, cat_template in cat_templates.items(): - _convert_template_item(cat_template) - if template_name == default_template_name: - templates[cat_key] = cat_template - else: - new_name = "{}_{}".format(cat_key, template_name) - templates["others"][new_name] = cat_template - - -def convert_v4_project_to_v3(project): - """Convert Project entity data from v4 structure to v3 structure. - - Args: - project (Dict[str, Any]): Project entity queried from v4 server. - - Returns: - Dict[str, Any]: Project converted to v3 structure. - """ - - if not project: - return project - - project_name = project["name"] - output = { - "_id": project_name, - "name": project_name, - "schema": CURRENT_PROJECT_SCHEMA, - "type": "project" - } - - data = project.get("data") or {} - attribs = project.get("attrib") or {} - apps_attr = attribs.pop("applications", None) or [] - applications = [ - {"name": app_name} - for app_name in apps_attr - ] - data.update(attribs) - if "tools" in data: - data["tools_env"] = data.pop("tools") - - data["entityType"] = "Project" - - config = {} - project_config = project.get("config") - - if project_config: - config["apps"] = applications - config["roots"] = project_config["roots"] - - templates = project_config["templates"] - templates["defaults"] = templates.pop("common", None) or {} - - others_templates = templates.pop("others", None) or {} - new_others_templates = {} - templates["others"] = new_others_templates - for name, template in others_templates.items(): - _convert_template_item(template) - new_others_templates[name] = template - - staging_templates = templates.pop("staging", None) - # Key 'staging_directories' is legacy key that changed - # to 'staging_dir' - _legacy_staging_templates = templates.pop("staging_directories", None) - if staging_templates is None: - staging_templates = _legacy_staging_templates - - if staging_templates is None: - staging_templates = {} - - # Prefix all staging template names with 'staging_' prefix - # and add them to 'others' - for name, template in staging_templates.items(): - _convert_template_item(template) - new_name = "staging_{}".format(name) - new_others_templates[new_name] = template - - for key in ( - "work", - "publish", - "hero", - ): - cat_templates = templates.pop(key) - _fill_template_category(templates, cat_templates, key) - - delivery_templates = templates.pop("delivery", None) or {} - new_delivery_templates = {} - for name, delivery_template in delivery_templates.items(): - new_delivery_templates[name] = "/".join( - (delivery_template["directory"], delivery_template["file"]) - ) - templates["delivery"] = new_delivery_templates - - config["templates"] = templates - - if "taskTypes" in project: - task_types = project["taskTypes"] - new_task_types = {} - for task_type in task_types: - name = task_type.pop("name") - # Change 'shortName' to 'short_name' - task_type["short_name"] = task_type.pop("shortName", None) - new_task_types[name] = task_type - - config["tasks"] = new_task_types - - if config: - output["config"] = config - - for data_key, key in ( - ("library_project", "library"), - ("code", "code"), - ("active", "active") - ): - if key in project: - data[data_key] = project[key] - - if "attrib" in project: - for key, value in project["attrib"].items(): - data[key] = value - - if data: - output["data"] = data - return output - - -def folder_fields_v3_to_v4(fields, con): - """Convert folder fields from v3 to v4 structure. - - Args: - fields (Union[Iterable(str), None]): fields to be converted. - - Returns: - Union[Set(str), None]: Converted fields to v4 fields. - """ - - if not fields: - return None - - folder_attributes = con.get_attributes_for_type("folder") - output = set() - for field in fields: - if field in ("schema", "type", "parent"): - continue - - if field in FOLDER_FIELDS_MAPPING_V3_V4: - output |= FOLDER_FIELDS_MAPPING_V3_V4[field] - if field == "data": - output |= { - "attrib.{}".format(attr) - for attr in folder_attributes - } - - elif field.startswith("data"): - field_parts = field.split(".") - field_parts.pop(0) - data_key = ".".join(field_parts) - if data_key == "label": - output.add("name") - - elif data_key in ("icon", "color"): - continue - - elif data_key.startswith("tasks"): - output.add("tasks") - - elif data_key in folder_attributes: - output.add("attrib.{}".format(data_key)) - - else: - output.add("data") - print("Requested specific key from data {}".format(data_key)) - - else: - raise ValueError("Unknown field mapping for {}".format(field)) - - if "id" not in output: - output.add("id") - return output - - -def convert_v4_tasks_to_v3(tasks): - """Convert v4 task item to v3 task. - - Args: - tasks (List[Dict[str, Any]]): Task entites. - - Returns: - Dict[str, Dict[str, Any]]: Tasks in v3 variant ready for v3 asset. - """ - - output = {} - for task in tasks: - task_name = task["name"] - new_task = { - "type": task["taskType"] - } - output[task_name] = new_task - return output - - -def convert_v4_folder_to_v3(folder, project_name): - """Convert v4 folder to v3 asset. - - Args: - folder (Dict[str, Any]): Folder entity data. - project_name (str): Project name from which folder was queried. - - Returns: - Dict[str, Any]: Converted v4 folder to v3 asset. - """ - - output = { - "_id": folder["id"], - "parent": project_name, - "type": "asset", - "schema": CURRENT_ASSET_DOC_SCHEMA - } - - output_data = folder.get("data") or {} - - if "name" in folder: - output["name"] = folder["name"] - output_data["label"] = folder["name"] - - if "folderType" in folder: - output_data["entityType"] = folder["folderType"] - - for src_key, dst_key in ( - ("parentId", "visualParent"), - ("active", "active"), - ("thumbnailId", "thumbnail_id"), - ("parents", "parents"), - ): - if src_key in folder: - output_data[dst_key] = folder[src_key] - - if "attrib" in folder: - output_data.update(folder["attrib"]) - - if "tools" in output_data: - output_data["tools_env"] = output_data.pop("tools") - - if "tasks" in folder: - output_data["tasks"] = convert_v4_tasks_to_v3(folder["tasks"]) - - output["data"] = output_data - - return output - - -def subset_fields_v3_to_v4(fields, con): - """Convert subset fields from v3 to v4 structure. - - Args: - fields (Union[Iterable(str), None]): fields to be converted. - - Returns: - Union[Set(str), None]: Converted fields to v4 fields. - """ - - if not fields: - return None - - product_attributes = con.get_attributes_for_type("product") - - output = set() - for field in fields: - if field in ("schema", "type"): - continue - - if field in SUBSET_FIELDS_MAPPING_V3_V4: - output |= SUBSET_FIELDS_MAPPING_V3_V4[field] - - elif field == "data": - output.add("productType") - output.add("active") - output |= { - "attrib.{}".format(attr) - for attr in product_attributes - } - - elif field.startswith("data"): - field_parts = field.split(".") - field_parts.pop(0) - data_key = ".".join(field_parts) - if data_key in ("family", "families"): - output.add("productType") - - elif data_key in product_attributes: - output.add("attrib.{}".format(data_key)) - - else: - output.add("data") - print("Requested specific key from data {}".format(data_key)) - - else: - raise ValueError("Unknown field mapping for {}".format(field)) - - if "id" not in output: - output.add("id") - return output - - -def convert_v4_subset_to_v3(subset): - output = { - "_id": subset["id"], - "type": "subset", - "schema": CURRENT_SUBSET_SCHEMA - } - if "folderId" in subset: - output["parent"] = subset["folderId"] - - output_data = subset.get("data") or {} - - if "name" in subset: - output["name"] = subset["name"] - - if "active" in subset: - output_data["active"] = subset["active"] - - if "attrib" in subset: - attrib = subset["attrib"] - if "productGroup" in attrib: - attrib["subsetGroup"] = attrib.pop("productGroup") - output_data.update(attrib) - - family = subset.get("productType") - if family: - output_data["family"] = family - output_data["families"] = [family] - - output["data"] = output_data - - return output - - -def version_fields_v3_to_v4(fields, con): - """Convert version fields from v3 to v4 structure. - - Args: - fields (Union[Iterable(str), None]): fields to be converted. - - Returns: - Union[Set(str), None]: Converted fields to v4 fields. - """ - - if not fields: - return None - - version_attributes = con.get_attributes_for_type("version") - - output = set() - for field in fields: - if field in ("type", "schema", "version_id"): - continue - - if field in VERSION_FIELDS_MAPPING_V3_V4: - output |= VERSION_FIELDS_MAPPING_V3_V4[field] - - elif field == "data": - output |= { - "attrib.{}".format(attr) - for attr in version_attributes - } - output |= { - "author", - "createdAt", - "thumbnailId", - } - - elif field.startswith("data"): - field_parts = field.split(".") - field_parts.pop(0) - data_key = ".".join(field_parts) - if data_key in version_attributes: - output.add("attrib.{}".format(data_key)) - - elif data_key == "thumbnail_id": - output.add("thumbnailId") - - elif data_key == "time": - output.add("createdAt") - - elif data_key == "author": - output.add("author") - - elif data_key in ("tags", ): - continue - - else: - output.add("data") - print("Requested specific key from data {}".format(data_key)) - - else: - raise ValueError("Unknown field mapping for {}".format(field)) - - if "id" not in output: - output.add("id") - return output - - -def convert_v4_version_to_v3(version): - """Convert v4 version entity to v4 version. - - Args: - version (Dict[str, Any]): Queried v4 version entity. - - Returns: - Dict[str, Any]: Conveted version entity to v3 structure. - """ - - version_num = version["version"] - if version_num < 0: - output = { - "_id": version["id"], - "type": "hero_version", - "schema": CURRENT_HERO_VERSION_SCHEMA, - } - if "productId" in version: - output["parent"] = version["productId"] - - if "data" in version: - output["data"] = version["data"] - return output - - output = { - "_id": version["id"], - "type": "version", - "name": version_num, - "schema": CURRENT_VERSION_SCHEMA - } - if "productId" in version: - output["parent"] = version["productId"] - - output_data = version.get("data") or {} - if "attrib" in version: - output_data.update(version["attrib"]) - - for src_key, dst_key in ( - ("active", "active"), - ("thumbnailId", "thumbnail_id"), - ("author", "author") - ): - if src_key in version: - output_data[dst_key] = version[src_key] - - if "createdAt" in version: - created_at = arrow.get(version["createdAt"]).to("local") - output_data["time"] = created_at.strftime("%Y%m%dT%H%M%SZ") - - output["data"] = output_data - - return output - - -def representation_fields_v3_to_v4(fields, con): - """Convert representation fields from v3 to v4 structure. - - Args: - fields (Union[Iterable(str), None]): fields to be converted. - - Returns: - Union[Set(str), None]: Converted fields to v4 fields. - """ - - if not fields: - return None - - representation_attributes = con.get_attributes_for_type("representation") - - output = set() - for field in fields: - if field in ("type", "schema"): - continue - - if field in REPRESENTATION_FIELDS_MAPPING_V3_V4: - output |= REPRESENTATION_FIELDS_MAPPING_V3_V4[field] - - elif field.startswith("context"): - output.add("context") - - # TODO: 'files' can have specific attributes but the keys in v3 and v4 - # are not the same (content is not the same) - elif field.startswith("files"): - output |= REPRESENTATION_FILES_FIELDS - - elif field.startswith("data"): - output |= { - "attrib.{}".format(attr) - for attr in representation_attributes - } - - else: - raise ValueError("Unknown field mapping for {}".format(field)) - - if "id" not in output: - output.add("id") - return output - - -def convert_v4_representation_to_v3(representation): - """Convert v4 representation to v3 representation. - - Args: - representation (Dict[str, Any]): Queried representation from v4 server. - - Returns: - Dict[str, Any]: Converted representation to v3 structure. - """ - - output = { - "type": "representation", - "schema": CURRENT_REPRESENTATION_SCHEMA, - } - if "id" in representation: - output["_id"] = representation["id"] - - for v3_key, v4_key in ( - ("name", "name"), - ("parent", "versionId") - ): - if v4_key in representation: - output[v3_key] = representation[v4_key] - - if "context" in representation: - context = representation["context"] - if isinstance(context, six.string_types): - context = json.loads(context) - - if "asset" not in context and "folder" in context: - _c_folder = context["folder"] - context["asset"] = _c_folder["name"] - - elif "asset" in context and "folder" not in context: - context["folder"] = {"name": context["asset"]} - - if "product" in context: - _c_product = context.pop("product") - context["family"] = _c_product["type"] - context["subset"] = _c_product["name"] - - output["context"] = context - - if "files" in representation: - files = representation["files"] - new_files = [] - # From GraphQl is list - if isinstance(files, list): - for file_info in files: - file_info["_id"] = file_info["id"] - new_files.append(file_info) - - # From RestPoint is dictionary - elif isinstance(files, dict): - for file_id, file_info in files: - file_info["_id"] = file_id - new_files.append(file_info) - - for file_info in new_files: - if not file_info.get("sites"): - file_info["sites"] = [{ - "name": "studio" - }] - - output["files"] = new_files - - if representation.get("active") is False: - output["type"] = "archived_representation" - output["old_id"] = output["_id"] - - output_data = representation.get("data") or {} - if "attrib" in representation: - output_data.update(representation["attrib"]) - - for key, data_key in ( - ("active", "active"), - ): - if key in representation: - output_data[data_key] = representation[key] - - if "template" in output_data: - output_data["template"] = ( - output_data["template"] - .replace("{product[name]}", "{subset}") - .replace("{product[type]}", "{family}") - ) - - output["data"] = output_data - - return output - - -def workfile_info_fields_v3_to_v4(fields): - if not fields: - return None - - new_fields = set() - fields = set(fields) - for v3_key, v4_key in ( - ("_id", "id"), - ("files", "path"), - ("filename", "name"), - ("data", "data"), - ): - if v3_key in fields: - new_fields.add(v4_key) - - if "parent" in fields or "task_name" in fields: - new_fields.add("taskId") - - return new_fields - - -def convert_v4_workfile_info_to_v3(workfile_info, task): - output = { - "type": "workfile", - "schema": CURRENT_WORKFILE_INFO_SCHEMA, - } - if "id" in workfile_info: - output["_id"] = workfile_info["id"] - - if "path" in workfile_info: - output["files"] = [workfile_info["path"]] - - if "name" in workfile_info: - output["filename"] = workfile_info["name"] - - if "taskId" in workfile_info: - output["task_name"] = task["name"] - output["parent"] = task["folderId"] - - return output - - -def convert_create_asset_to_v4(asset, project, con): - folder_attributes = con.get_attributes_for_type("folder") - - asset_data = asset["data"] - parent_id = asset_data["visualParent"] - - folder = { - "name": asset["name"], - "parentId": parent_id, - } - entity_id = asset.get("_id") - if entity_id: - folder["id"] = entity_id - - attribs = {} - data = {} - for key, value in asset_data.items(): - if key in ( - "visualParent", - "thumbnail_id", - "parents", - "inputLinks", - "avalon_mongo_id", - ): - continue - - if key not in folder_attributes: - data[key] = value - elif value is not None: - attribs[key] = value - - if attribs: - folder["attrib"] = attribs - - if data: - folder["data"] = data - return folder - - -def convert_create_task_to_v4(task, project, con): - if not project["taskTypes"]: - raise ValueError( - "Project \"{}\" does not have any task types".format( - project["name"])) - - task_type = task["type"] - if task_type not in project["taskTypes"]: - task_type = tuple(project["taskTypes"].keys())[0] - - return { - "name": task["name"], - "taskType": task_type, - "folderId": task["folderId"] - } - - -def convert_create_subset_to_v4(subset, con): - product_attributes = con.get_attributes_for_type("product") - - subset_data = subset["data"] - product_type = subset_data.get("family") - if not product_type: - product_type = subset_data["families"][0] - - converted_product = { - "name": subset["name"], - "productType": product_type, - "folderId": subset["parent"], - } - entity_id = subset.get("_id") - if entity_id: - converted_product["id"] = entity_id - - attribs = {} - data = {} - if "subsetGroup" in subset_data: - subset_data["productGroup"] = subset_data.pop("subsetGroup") - for key, value in subset_data.items(): - if key not in product_attributes: - data[key] = value - elif value is not None: - attribs[key] = value - - if attribs: - converted_product["attrib"] = attribs - - if data: - converted_product["data"] = data - - return converted_product - - -def convert_create_version_to_v4(version, con): - version_attributes = con.get_attributes_for_type("version") - converted_version = { - "version": version["name"], - "productId": version["parent"], - } - entity_id = version.get("_id") - if entity_id: - converted_version["id"] = entity_id - - version_data = version["data"] - attribs = {} - data = {} - for key, value in version_data.items(): - if key not in version_attributes: - data[key] = value - elif value is not None: - attribs[key] = value - - if attribs: - converted_version["attrib"] = attribs - - if data: - converted_version["data"] = attribs - - return converted_version - - -def convert_create_hero_version_to_v4(hero_version, project_name, con): - if "version_id" in hero_version: - version_id = hero_version["version_id"] - version = con.get_version_by_id(project_name, version_id) - version["version"] = - version["version"] - - for auto_key in ( - "name", - "createdAt", - "updatedAt", - "author", - ): - version.pop(auto_key, None) - - return version - - version_attributes = con.get_attributes_for_type("version") - converted_version = { - "version": hero_version["version"], - "productId": hero_version["parent"], - } - entity_id = hero_version.get("_id") - if entity_id: - converted_version["id"] = entity_id - - version_data = hero_version["data"] - attribs = {} - data = {} - for key, value in version_data.items(): - if key not in version_attributes: - data[key] = value - elif value is not None: - attribs[key] = value - - if attribs: - converted_version["attrib"] = attribs - - if data: - converted_version["data"] = attribs - - return converted_version - - -def convert_create_representation_to_v4(representation, con): - representation_attributes = con.get_attributes_for_type("representation") - - converted_representation = { - "name": representation["name"], - "versionId": representation["parent"], - } - entity_id = representation.get("_id") - if entity_id: - converted_representation["id"] = entity_id - - if representation.get("type") == "archived_representation": - converted_representation["active"] = False - - new_files = [] - for file_item in representation["files"]: - new_file_item = { - key: value - for key, value in file_item.items() - if key in ("hash", "path", "size") - } - new_file_item.update({ - "id": create_entity_id(), - "hash_type": "op3", - "name": os.path.basename(new_file_item["path"]) - }) - new_files.append(new_file_item) - - converted_representation["files"] = new_files - - context = representation["context"] - if "folder" not in context: - context["folder"] = { - "name": context.get("asset") - } - - context["product"] = { - "type": context.pop("family", None), - "name": context.pop("subset", None), - } - - attribs = {} - data = { - "context": context, - } - - representation_data = representation["data"] - representation_data["template"] = ( - representation_data["template"] - .replace("{subset}", "{product[name]}") - .replace("{family}", "{product[type]}") - ) - - for key, value in representation_data.items(): - if key not in representation_attributes: - data[key] = value - elif value is not None: - attribs[key] = value - - if attribs: - converted_representation["attrib"] = attribs - - if data: - converted_representation["data"] = data - - return converted_representation - - -def convert_create_workfile_info_to_v4(data, project_name, con): - folder_id = data["parent"] - task_name = data["task_name"] - task = con.get_task_by_name(project_name, folder_id, task_name) - if not task: - return None - - workfile_attributes = con.get_attributes_for_type("workfile") - filename = data["filename"] - possible_attribs = { - "extension": os.path.splitext(filename)[-1] - } - attribs = {} - for attr in workfile_attributes: - if attr in possible_attribs: - attribs[attr] = possible_attribs[attr] - - output = { - "path": data["files"][0], - "name": filename, - "taskId": task["id"] - } - if "_id" in data: - output["id"] = data["_id"] - - if attribs: - output["attrib"] = attribs - - output_data = data.get("data") - if output_data: - output["data"] = output_data - return output - - -def _from_flat_dict(data): - output = {} - for key, value in data.items(): - output_value = output - subkeys = key.split(".") - last_key = subkeys.pop(-1) - for subkey in subkeys: - if subkey not in output_value: - output_value[subkey] = {} - output_value = output_value[subkey] - - output_value[last_key] = value - return output - - -def _to_flat_dict(data): - output = {} - flat_queue = collections.deque() - flat_queue.append(([], data)) - while flat_queue: - item = flat_queue.popleft() - parent_keys, data = item - for key, value in data.items(): - keys = list(parent_keys) - keys.append(key) - if isinstance(value, dict): - flat_queue.append((keys, value)) - else: - full_key = ".".join(keys) - output[full_key] = value - - return output - - -def convert_update_folder_to_v4(project_name, asset_id, update_data, con): - new_update_data = {} - - folder_attributes = con.get_attributes_for_type("folder") - full_update_data = _from_flat_dict(update_data) - data = full_update_data.get("data") - - has_new_parent = False - has_task_changes = False - parent_id = None - tasks = None - new_data = {} - attribs = full_update_data.pop("attrib", {}) - if "type" in update_data: - new_update_data["active"] = update_data["type"] == "asset" - - if data: - if "thumbnail_id" in data: - new_update_data["thumbnailId"] = data.pop("thumbnail_id") - - if "tasks" in data: - tasks = data.pop("tasks") - has_task_changes = True - - if "visualParent" in data: - has_new_parent = True - parent_id = data.pop("visualParent") - - for key, value in data.items(): - if key in folder_attributes: - attribs[key] = value - else: - new_data[key] = value - - if "name" in update_data: - new_update_data["name"] = update_data["name"] - - if "type" in update_data: - new_type = update_data["type"] - if new_type == "asset": - new_update_data["active"] = True - elif new_type == "archived_asset": - new_update_data["active"] = False - - if has_new_parent: - new_update_data["parentId"] = parent_id - - if new_data: - print("Folder has new data: {}".format(new_data)) - new_update_data["data"] = new_data - - if attribs: - new_update_data["attrib"] = attribs - - if has_task_changes: - raise ValueError("Task changes of folder are not implemented") - - return _to_flat_dict(new_update_data) - - -def convert_update_subset_to_v4(project_name, subset_id, update_data, con): - new_update_data = {} - - product_attributes = con.get_attributes_for_type("product") - full_update_data = _from_flat_dict(update_data) - data = full_update_data.get("data") - new_data = {} - attribs = full_update_data.pop("attrib", {}) - if data: - if "family" in data: - family = data.pop("family") - new_update_data["productType"] = family - - if "families" in data: - families = data.pop("families") - if "productType" not in new_update_data: - new_update_data["productType"] = families[0] - - if "subsetGroup" in data: - data["productGroup"] = data.pop("subsetGroup") - for key, value in data.items(): - if key in product_attributes: - if value is REMOVED_VALUE: - value = None - attribs[key] = value - - elif value is not REMOVED_VALUE: - new_data[key] = value - - if "name" in update_data: - new_update_data["name"] = update_data["name"] - - if "type" in update_data: - new_type = update_data["type"] - if new_type == "subset": - new_update_data["active"] = True - elif new_type == "archived_subset": - new_update_data["active"] = False - - if "parent" in update_data: - new_update_data["folderId"] = update_data["parent"] - - flat_data = _to_flat_dict(new_update_data) - if attribs: - flat_data["attrib"] = attribs - - if new_data: - print("Subset has new data: {}".format(new_data)) - flat_data["data"] = new_data - - return flat_data - - -def convert_update_version_to_v4(project_name, version_id, update_data, con): - new_update_data = {} - - version_attributes = con.get_attributes_for_type("version") - full_update_data = _from_flat_dict(update_data) - data = full_update_data.get("data") - new_data = {} - attribs = full_update_data.pop("attrib", {}) - if data: - if "author" in data: - new_update_data["author"] = data.pop("author") - - if "thumbnail_id" in data: - new_update_data["thumbnailId"] = data.pop("thumbnail_id") - - for key, value in data.items(): - if key in version_attributes: - if value is REMOVED_VALUE: - value = None - attribs[key] = value - - elif value is not REMOVED_VALUE: - new_data[key] = value - - if "name" in update_data: - new_update_data["version"] = update_data["name"] - - if "type" in update_data: - new_type = update_data["type"] - if new_type == "version": - new_update_data["active"] = True - elif new_type == "archived_version": - new_update_data["active"] = False - - if "parent" in update_data: - new_update_data["productId"] = update_data["parent"] - - flat_data = _to_flat_dict(new_update_data) - if attribs: - flat_data["attrib"] = attribs - - if new_data: - print("Version has new data: {}".format(new_data)) - flat_data["data"] = new_data - return flat_data - - -def convert_update_hero_version_to_v4( - project_name, hero_version_id, update_data, con -): - if "version_id" not in update_data: - return None - - version_id = update_data["version_id"] - hero_version = con.get_hero_version_by_id(project_name, hero_version_id) - version = con.get_version_by_id(project_name, version_id) - version["version"] = - version["version"] - version["id"] = hero_version_id - - for auto_key in ( - "name", - "createdAt", - "updatedAt", - "author", - ): - version.pop(auto_key, None) - - return prepare_entity_changes(hero_version, version) - - -def convert_update_representation_to_v4( - project_name, repre_id, update_data, con -): - new_update_data = {} - - folder_attributes = con.get_attributes_for_type("folder") - full_update_data = _from_flat_dict(update_data) - data = full_update_data.get("data") - - new_data = {} - attribs = full_update_data.pop("attrib", {}) - if data: - for key, value in data.items(): - if key in folder_attributes: - attribs[key] = value - else: - new_data[key] = value - - if "template" in attribs: - attribs["template"] = ( - attribs["template"] - .replace("{family}", "{product[type]}") - .replace("{subset}", "{product[name]}") - ) - - if "name" in update_data: - new_update_data["name"] = update_data["name"] - - if "type" in update_data: - new_type = update_data["type"] - if new_type == "representation": - new_update_data["active"] = True - elif new_type == "archived_representation": - new_update_data["active"] = False - - if "parent" in update_data: - new_update_data["versionId"] = update_data["parent"] - - if "context" in update_data: - context = update_data["context"] - if "folder" not in context and "asset" in context: - context["folder"] = {"name": context.pop("asset")} - - if "family" in context or "subset" in context: - context["product"] = { - "name": context.pop("subset"), - "type": context.pop("family"), - } - new_data["context"] = context - - if "files" in update_data: - new_files = update_data["files"] - if isinstance(new_files, dict): - new_files = list(new_files.values()) - - for item in new_files: - for key in tuple(item.keys()): - if key not in ("hash", "path", "size"): - item.pop(key) - item.update({ - "id": create_entity_id(), - "name": os.path.basename(item["path"]), - "hash_type": "op3", - }) - new_update_data["files"] = new_files - - flat_data = _to_flat_dict(new_update_data) - if attribs: - flat_data["attrib"] = attribs - - if new_data: - print("Representation has new data: {}".format(new_data)) - flat_data["data"] = new_data - - return flat_data - - -def convert_update_workfile_info_to_v4( - project_name, workfile_id, update_data, con -): - return { - key: value - for key, value in update_data.items() - if key.startswith("data") - } diff --git a/client/ayon_core/client/entities.py b/client/ayon_core/client/entities.py deleted file mode 100644 index 5ef2571421..0000000000 --- a/client/ayon_core/client/entities.py +++ /dev/null @@ -1,741 +0,0 @@ -import collections - -from .constants import CURRENT_THUMBNAIL_SCHEMA -from .utils import get_ayon_server_api_connection -from .openpype_comp import get_folders_with_tasks -from .conversion_utils import ( - project_fields_v3_to_v4, - convert_v4_project_to_v3, - - folder_fields_v3_to_v4, - convert_v4_folder_to_v3, - - subset_fields_v3_to_v4, - convert_v4_subset_to_v3, - - version_fields_v3_to_v4, - convert_v4_version_to_v3, - - representation_fields_v3_to_v4, - convert_v4_representation_to_v3, - - workfile_info_fields_v3_to_v4, - convert_v4_workfile_info_to_v3, -) - - -def get_asset_name_identifier(asset_doc): - """Get asset name identifier by asset document. - - This function is added because of AYON implementation where name - identifier is not just a name but full path. - - Asset document must have "name" key, and "data.parents" when in AYON mode. - - Args: - asset_doc (dict[str, Any]): Asset document. - """ - - parents = list(asset_doc["data"]["parents"]) - parents.append(asset_doc["name"]) - return "/" + "/".join(parents) - - -def get_projects(active=True, inactive=False, library=None, fields=None): - if not active and not inactive: - return - - if active and inactive: - active = None - elif active: - active = True - elif inactive: - active = False - - con = get_ayon_server_api_connection() - fields = project_fields_v3_to_v4(fields, con) - for project in con.get_projects(active, library, fields=fields): - yield convert_v4_project_to_v3(project) - - -def get_project(project_name, active=True, inactive=False, fields=None): - # Skip if both are disabled - con = get_ayon_server_api_connection() - fields = project_fields_v3_to_v4(fields, con) - return convert_v4_project_to_v3( - con.get_project(project_name, fields=fields) - ) - - -def get_whole_project(*args, **kwargs): - raise NotImplementedError("'get_whole_project' not implemented") - - -def _get_subsets( - project_name, - subset_ids=None, - subset_names=None, - folder_ids=None, - names_by_folder_ids=None, - archived=False, - fields=None -): - # Convert fields and add minimum required fields - con = get_ayon_server_api_connection() - fields = subset_fields_v3_to_v4(fields, con) - if fields is not None: - for key in ( - "id", - "active" - ): - fields.add(key) - - active = True - if archived: - active = None - - for subset in con.get_products( - project_name, - product_ids=subset_ids, - product_names=subset_names, - folder_ids=folder_ids, - names_by_folder_ids=names_by_folder_ids, - active=active, - fields=fields, - ): - yield convert_v4_subset_to_v3(subset) - - -def _get_versions( - project_name, - version_ids=None, - subset_ids=None, - versions=None, - hero=True, - standard=True, - latest=None, - active=None, - fields=None -): - con = get_ayon_server_api_connection() - - fields = version_fields_v3_to_v4(fields, con) - - # Make sure 'productId' and 'version' are available when hero versions - # are queried - if fields and hero: - fields = set(fields) - fields |= {"productId", "version"} - - queried_versions = con.get_versions( - project_name, - version_ids=version_ids, - product_ids=subset_ids, - versions=versions, - hero=hero, - standard=standard, - latest=latest, - active=active, - fields=fields - ) - - version_entities = [] - hero_versions = [] - for version in queried_versions: - if version["version"] < 0: - hero_versions.append(version) - else: - version_entities.append(convert_v4_version_to_v3(version)) - - if hero_versions: - subset_ids = set() - versions_nums = set() - for hero_version in hero_versions: - versions_nums.add(abs(hero_version["version"])) - subset_ids.add(hero_version["productId"]) - - hero_eq_versions = con.get_versions( - project_name, - product_ids=subset_ids, - versions=versions_nums, - hero=False, - fields=["id", "version", "productId"] - ) - hero_eq_by_subset_id = collections.defaultdict(list) - for version in hero_eq_versions: - hero_eq_by_subset_id[version["productId"]].append(version) - - for hero_version in hero_versions: - abs_version = abs(hero_version["version"]) - subset_id = hero_version["productId"] - version_id = None - for version in hero_eq_by_subset_id.get(subset_id, []): - if version["version"] == abs_version: - version_id = version["id"] - break - conv_hero = convert_v4_version_to_v3(hero_version) - conv_hero["version_id"] = version_id - version_entities.append(conv_hero) - - return version_entities - - -def get_asset_by_id(project_name, asset_id, fields=None): - assets = get_assets( - project_name, asset_ids=[asset_id], fields=fields - ) - for asset in assets: - return asset - return None - - -def get_asset_by_name(project_name, asset_name, fields=None): - assets = get_assets( - project_name, asset_names=[asset_name], fields=fields - ) - for asset in assets: - return asset - return None - - -def _folders_query(project_name, con, fields, **kwargs): - if fields is None or "tasks" in fields: - folders = get_folders_with_tasks( - con, project_name, fields=fields, **kwargs - ) - - else: - folders = con.get_folders(project_name, fields=fields, **kwargs) - - for folder in folders: - yield folder - - -def get_assets( - project_name, - asset_ids=None, - asset_names=None, - parent_ids=None, - archived=False, - fields=None -): - if not project_name: - return - - active = True - if archived: - active = None - - con = get_ayon_server_api_connection() - fields = folder_fields_v3_to_v4(fields, con) - kwargs = dict( - folder_ids=asset_ids, - parent_ids=parent_ids, - active=active, - ) - if not asset_names: - for folder in _folders_query(project_name, con, fields, **kwargs): - yield convert_v4_folder_to_v3(folder, project_name) - return - - new_asset_names = set() - folder_paths = set() - for name in asset_names: - if "/" in name: - folder_paths.add(name) - else: - new_asset_names.add(name) - - yielded_ids = set() - if folder_paths: - for folder in _folders_query( - project_name, con, fields, folder_paths=folder_paths, **kwargs - ): - yielded_ids.add(folder["id"]) - yield convert_v4_folder_to_v3(folder, project_name) - - if not new_asset_names: - return - - for folder in _folders_query( - project_name, con, fields, folder_names=new_asset_names, **kwargs - ): - if folder["id"] not in yielded_ids: - yielded_ids.add(folder["id"]) - yield convert_v4_folder_to_v3(folder, project_name) - - -def get_archived_assets( - project_name, - asset_ids=None, - asset_names=None, - parent_ids=None, - fields=None -): - return get_assets( - project_name, - asset_ids, - asset_names, - parent_ids, - True, - fields - ) - - -def get_asset_ids_with_subsets(project_name, asset_ids=None): - con = get_ayon_server_api_connection() - return con.get_folder_ids_with_products(project_name, asset_ids) - - -def get_subset_by_id(project_name, subset_id, fields=None): - subsets = get_subsets( - project_name, subset_ids=[subset_id], fields=fields - ) - for subset in subsets: - return subset - return None - - -def get_subset_by_name(project_name, subset_name, asset_id, fields=None): - subsets = get_subsets( - project_name, - subset_names=[subset_name], - asset_ids=[asset_id], - fields=fields - ) - for subset in subsets: - return subset - return None - - -def get_subsets( - project_name, - subset_ids=None, - subset_names=None, - asset_ids=None, - names_by_asset_ids=None, - archived=False, - fields=None -): - return _get_subsets( - project_name, - subset_ids, - subset_names, - asset_ids, - names_by_asset_ids, - archived, - fields=fields - ) - - -def get_subset_families(project_name, subset_ids=None): - con = get_ayon_server_api_connection() - return con.get_product_type_names(project_name, subset_ids) - - -def get_version_by_id(project_name, version_id, fields=None): - versions = get_versions( - project_name, - version_ids=[version_id], - fields=fields, - hero=True - ) - for version in versions: - return version - return None - - -def get_version_by_name(project_name, version, subset_id, fields=None): - versions = get_versions( - project_name, - subset_ids=[subset_id], - versions=[version], - fields=fields - ) - for version in versions: - return version - return None - - -def get_versions( - project_name, - version_ids=None, - subset_ids=None, - versions=None, - hero=False, - fields=None -): - return _get_versions( - project_name, - version_ids, - subset_ids, - versions, - hero=hero, - standard=True, - fields=fields - ) - - -def get_hero_version_by_id(project_name, version_id, fields=None): - versions = get_hero_versions( - project_name, - version_ids=[version_id], - fields=fields - ) - for version in versions: - return version - return None - - -def get_hero_version_by_subset_id( - project_name, subset_id, fields=None -): - versions = get_hero_versions( - project_name, - subset_ids=[subset_id], - fields=fields - ) - for version in versions: - return version - return None - - -def get_hero_versions( - project_name, subset_ids=None, version_ids=None, fields=None -): - return _get_versions( - project_name, - version_ids=version_ids, - subset_ids=subset_ids, - hero=True, - standard=False, - fields=fields - ) - - -def get_last_versions(project_name, subset_ids, active=None, fields=None): - if fields: - fields = set(fields) - fields.add("parent") - - versions = _get_versions( - project_name, - subset_ids=subset_ids, - latest=True, - hero=False, - active=active, - fields=fields - ) - return { - version["parent"]: version - for version in versions - } - - -def get_last_version_by_subset_id(project_name, subset_id, fields=None): - versions = _get_versions( - project_name, - subset_ids=[subset_id], - latest=True, - hero=False, - fields=fields - ) - if not versions: - return None - return versions[0] - - -def get_last_version_by_subset_name( - project_name, - subset_name, - asset_id=None, - asset_name=None, - fields=None -): - if not asset_id and not asset_name: - return None - - if not asset_id: - asset = get_asset_by_name( - project_name, asset_name, fields=["_id"] - ) - if not asset: - return None - asset_id = asset["_id"] - - subset = get_subset_by_name( - project_name, subset_name, asset_id, fields=["_id"] - ) - if not subset: - return None - return get_last_version_by_subset_id( - project_name, subset["_id"], fields=fields - ) - - -def get_output_link_versions(project_name, version_id, fields=None): - if not version_id: - return [] - - con = get_ayon_server_api_connection() - version_links = con.get_version_links( - project_name, version_id, link_direction="out") - - version_ids = { - link["entityId"] - for link in version_links - if link["entityType"] == "version" - } - if not version_ids: - return [] - - return get_versions(project_name, version_ids=version_ids, fields=fields) - - -def version_is_latest(project_name, version_id): - con = get_ayon_server_api_connection() - return con.version_is_latest(project_name, version_id) - - -def get_representation_by_id(project_name, representation_id, fields=None): - representations = get_representations( - project_name, - representation_ids=[representation_id], - fields=fields - ) - for representation in representations: - return representation - return None - - -def get_representation_by_name( - project_name, representation_name, version_id, fields=None -): - representations = get_representations( - project_name, - representation_names=[representation_name], - version_ids=[version_id], - fields=fields - ) - for representation in representations: - return representation - return None - - -def get_representations( - project_name, - representation_ids=None, - representation_names=None, - version_ids=None, - context_filters=None, - names_by_version_ids=None, - archived=False, - standard=True, - fields=None -): - if context_filters is not None: - # TODO should we add the support? - # - there was ability to fitler using regex - raise ValueError("OP v4 can't filter by representation context.") - - if not archived and not standard: - return - - if archived and not standard: - active = False - elif not archived and standard: - active = True - else: - active = None - - con = get_ayon_server_api_connection() - fields = representation_fields_v3_to_v4(fields, con) - if fields and active is not None: - fields.add("active") - - representations = con.get_representations( - project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - names_by_version_ids=names_by_version_ids, - active=active, - fields=fields - ) - for representation in representations: - yield convert_v4_representation_to_v3(representation) - - -def get_representation_parents(project_name, representation): - if not representation: - return None - - repre_id = representation["_id"] - parents_by_repre_id = get_representations_parents( - project_name, [representation] - ) - return parents_by_repre_id[repre_id] - - -def get_representations_parents(project_name, representations): - repre_ids = { - repre["_id"] - for repre in representations - } - con = get_ayon_server_api_connection() - parents_by_repre_id = con.get_representations_parents(project_name, - repre_ids) - folder_ids = set() - for parents in parents_by_repre_id .values(): - folder_ids.add(parents[2]["id"]) - - tasks_by_folder_id = {} - - new_parents = {} - for repre_id, parents in parents_by_repre_id .items(): - version, subset, folder, project = parents - folder_tasks = tasks_by_folder_id.get(folder["id"]) or {} - folder["tasks"] = folder_tasks - new_parents[repre_id] = ( - convert_v4_version_to_v3(version), - convert_v4_subset_to_v3(subset), - convert_v4_folder_to_v3(folder, project_name), - project - ) - return new_parents - - -def get_archived_representations( - project_name, - representation_ids=None, - representation_names=None, - version_ids=None, - context_filters=None, - names_by_version_ids=None, - fields=None -): - return get_representations( - project_name, - representation_ids=representation_ids, - representation_names=representation_names, - version_ids=version_ids, - context_filters=context_filters, - names_by_version_ids=names_by_version_ids, - archived=True, - standard=False, - fields=fields - ) - - -def get_thumbnail( - project_name, thumbnail_id, entity_type, entity_id, fields=None -): - """Receive thumbnail entity data. - - Args: - project_name (str): Name of project where to look for queried entities. - thumbnail_id (Union[str, ObjectId]): Id of thumbnail entity. - entity_type (str): Type of entity for which the thumbnail should be - received. - entity_id (str): Id of entity for which the thumbnail should be - received. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. - - Returns: - None: If thumbnail with specified id was not found. - Dict: Thumbnail entity data which can be reduced to specified 'fields'. - """ - - if not thumbnail_id or not entity_type or not entity_id: - return None - - if entity_type == "asset": - entity_type = "folder" - - elif entity_type == "hero_version": - entity_type = "version" - - return { - "_id": thumbnail_id, - "type": "thumbnail", - "schema": CURRENT_THUMBNAIL_SCHEMA, - "data": { - "entity_type": entity_type, - "entity_id": entity_id - } - } - - -def get_thumbnails(project_name, thumbnail_contexts, fields=None): - """Get thumbnail entities. - - Warning: - This function is not OpenPype compatible. There is none usage of this - function in codebase so there is nothing to convert. The previous - implementation cannot be AYON compatible without entity types. - """ - - thumbnail_items = set() - for thumbnail_context in thumbnail_contexts: - thumbnail_id, entity_type, entity_id = thumbnail_context - thumbnail_item = get_thumbnail( - project_name, thumbnail_id, entity_type, entity_id - ) - if thumbnail_item: - thumbnail_items.add(thumbnail_item) - return list(thumbnail_items) - - -def get_thumbnail_id_from_source(project_name, src_type, src_id): - """Receive thumbnail id from source entity. - - Args: - project_name (str): Name of project where to look for queried entities. - src_type (str): Type of source entity ('asset', 'version'). - src_id (Union[str, ObjectId]): Id of source entity. - - Returns: - ObjectId: Thumbnail id assigned to entity. - None: If Source entity does not have any thumbnail id assigned. - """ - - if not src_type or not src_id: - return None - - if src_type == "version": - version = get_version_by_id( - project_name, src_id, fields=["data.thumbnail_id"] - ) or {} - return version.get("data", {}).get("thumbnail_id") - - if src_type == "asset": - asset = get_asset_by_id( - project_name, src_id, fields=["data.thumbnail_id"] - ) or {} - return asset.get("data", {}).get("thumbnail_id") - - return None - - -def get_workfile_info( - project_name, asset_id, task_name, filename, fields=None -): - if not asset_id or not task_name or not filename: - return None - - con = get_ayon_server_api_connection() - task = con.get_task_by_name( - project_name, asset_id, task_name, fields=["id", "name", "folderId"] - ) - if not task: - return None - - fields = workfile_info_fields_v3_to_v4(fields) - - for workfile_info in con.get_workfiles_info( - project_name, task_ids=[task["id"]], fields=fields - ): - if workfile_info["name"] == filename: - return convert_v4_workfile_info_to_v3(workfile_info, task) - return None diff --git a/client/ayon_core/client/entity_links.py b/client/ayon_core/client/entity_links.py deleted file mode 100644 index 7fb9fbde6f..0000000000 --- a/client/ayon_core/client/entity_links.py +++ /dev/null @@ -1,157 +0,0 @@ -from .utils import get_ayon_server_api_connection -from .entities import get_assets, get_representation_by_id - - -def get_linked_asset_ids(project_name, asset_doc=None, asset_id=None): - """Extract linked asset ids from asset document. - - One of asset document or asset id must be passed. - - Note: - Asset links now works only from asset to assets. - - Args: - project_name (str): Project where to look for asset. - asset_doc (dict): Asset document from DB. - asset_id (str): Asset id to find its document. - - Returns: - List[Union[ObjectId, str]]: Asset ids of input links. - """ - - output = [] - if not asset_doc and not asset_id: - return output - - if not asset_id: - asset_id = asset_doc["_id"] - - con = get_ayon_server_api_connection() - links = con.get_folder_links(project_name, asset_id, link_direction="in") - return [ - link["entityId"] - for link in links - if link["entityType"] == "folder" - ] - - -def get_linked_assets( - project_name, asset_doc=None, asset_id=None, fields=None -): - """Return linked assets based on passed asset document. - - One of asset document or asset id must be passed. - - Args: - project_name (str): Name of project where to look for queried entities. - asset_doc (Dict[str, Any]): Asset document from database. - asset_id (Union[ObjectId, str]): Asset id. Can be used instead of - asset document. - fields (Iterable[str]): Fields that should be returned. All fields are - returned if 'None' is passed. - - Returns: - List[Dict[str, Any]]: Asset documents of input links for passed - asset doc. - """ - - link_ids = get_linked_asset_ids(project_name, asset_doc, asset_id) - if not link_ids: - return [] - return list(get_assets(project_name, asset_ids=link_ids, fields=fields)) - - - -def get_linked_representation_id( - project_name, repre_doc=None, repre_id=None, link_type=None, max_depth=None -): - """Returns list of linked ids of particular type (if provided). - - One of representation document or representation id must be passed. - Note: - Representation links now works only from representation through version - back to representations. - - Todos: - Missing depth query. Not sure how it did find more representations in - depth, probably links to version? - - Args: - project_name (str): Name of project where look for links. - repre_doc (Dict[str, Any]): Representation document. - repre_id (Union[ObjectId, str]): Representation id. - link_type (str): Type of link (e.g. 'reference', ...). - max_depth (int): Limit recursion level. Default: 0 - - Returns: - List[ObjectId] Linked representation ids. - """ - - if repre_doc: - repre_id = repre_doc["_id"] - - if not repre_id and not repre_doc: - return [] - - version_id = None - if repre_doc: - version_id = repre_doc.get("parent") - - if not version_id: - repre_doc = get_representation_by_id( - project_name, repre_id, fields=["parent"] - ) - if repre_doc: - version_id = repre_doc["parent"] - - if not version_id: - return [] - - if max_depth is None or max_depth == 0: - max_depth = 1 - - link_types = None - if link_type: - link_types = [link_type] - - con = get_ayon_server_api_connection() - # Store already found version ids to avoid recursion, and also to store - # output -> Don't forget to remove 'version_id' at the end!!! - linked_version_ids = {version_id} - # Each loop of depth will reset this variable - versions_to_check = {version_id} - for _ in range(max_depth): - if not versions_to_check: - break - - versions_links = con.get_versions_links( - project_name, - versions_to_check, - link_types=link_types, - link_direction="out") - - versions_to_check = set() - for links in versions_links.values(): - for link in links: - # Care only about version links - if link["entityType"] != "version": - continue - entity_id = link["entityId"] - # Skip already found linked version ids - if entity_id in linked_version_ids: - continue - linked_version_ids.add(entity_id) - versions_to_check.add(entity_id) - - linked_version_ids.remove(version_id) - if not linked_version_ids: - return [] - con = get_ayon_server_api_connection() - representations = con.get_representations( - project_name, - version_ids=linked_version_ids, - fields=["id"]) - return [ - repre["id"] - for repre in representations - ] diff --git a/client/ayon_core/client/notes.md b/client/ayon_core/client/notes.md deleted file mode 100644 index 59743892eb..0000000000 --- a/client/ayon_core/client/notes.md +++ /dev/null @@ -1,39 +0,0 @@ -# Client functionality -## Reason -Preparation for OpenPype v4 server. Goal is to remove direct mongo calls in code to prepare a little bit for different source of data for code before. To start think about database calls less as mongo calls but more universally. To do so was implemented simple wrapper around database calls to not use pymongo specific code. - -Current goal is not to make universal database model which can be easily replaced with any different source of data but to make it close as possible. Current implementation of OpenPype is too tightly connected to pymongo and it's abilities so we're trying to get closer with long term changes that can be used even in current state. - -## Queries -Query functions don't use full potential of mongo queries like very specific queries based on subdictionaries or unknown structures. We try to avoid these calls as much as possible because they'll probably won't be available in future. If it's really necessary a new function can be added but only if it's reasonable for overall logic. All query functions were moved to `~/client/entities.py`. Each function has arguments with available filters and possible reduce of returned keys for each entity. - -## Changes -Changes are a little bit complicated. Mongo has many options how update can happen which had to be reduced also it would be at this stage complicated to validate values which are created or updated thus automation is at this point almost none. Changes can be made using operations available in `~/client/operations.py`. Each operation require project name and entity type, but may require operation specific data. - -### Create -Create operations expect already prepared document data, for that are prepared functions creating skeletal structures of documents (do not fill all required data), except `_id` all data should be right. Existence of entity is not validated so if the same creation operation is send n times it will create the entity n times which can cause issues. - -### Update -Update operation require entity id and keys that should be changed, update dictionary must have {"key": value}. If value should be set in nested dictionary the key must have also all subkeys joined with dot `.` (e.g. `{"data": {"fps": 25}}` -> `{"data.fps": 25}`). To simplify update dictionaries were prepared functions which does that for you, their name has template `prepare__update_data` - they work on comparison of previous document and new document. If there is missing function for requested entity type it is because we didn't need it yet and require implementation. - -### Delete -Delete operation need entity id. Entity will be deleted from mongo. - - -## What (probably) won't be replaced -Some parts of code are still using direct mongo calls. In most of cases it is for very specific calls that are module specific or their usage will completely change in future. -- Mongo calls that are not project specific (out of `avalon` collection) will be removed or will have to use different mechanism how the data are stored. At this moment it is related to OpenPype settings and logs, ftrack server events, some other data. -- Sync server queries. They're complex and very specific for sync server module. Their replacement will require specific calls to OpenPype server in v4 thus their abstraction with wrapper is irrelevant and would complicate production in v3. -- Project managers (ftrack, kitsu, shotgrid, embedded Project Manager, etc.). Project managers are creating, updating or removing assets in v3, but in v4 will create folders with different structure. Wrapping creation of assets would not help to prepare for v4 because of new data structures. The same can be said about editorial Extract Hierarchy Avalon plugin which create project structure. -- Code parts that is marked as deprecated in v3 or will be deprecated in v4. - - integrate asset legacy publish plugin - already is legacy kept for safety - - integrate thumbnail - thumbnails will be stored in different way in v4 - - input links - link will be stored in different way and will have different mechanism of linking. In v3 are links limited to same entity type "asset <-> asset" or "representation <-> representation". - -## Known missing replacements -- change subset group in loader tool -- integrate subset group -- query input links in openpype lib -- create project in openpype lib -- save/create workfile doc in openpype lib -- integrate hero version diff --git a/client/ayon_core/client/openpype_comp.py b/client/ayon_core/client/openpype_comp.py deleted file mode 100644 index 71a141e913..0000000000 --- a/client/ayon_core/client/openpype_comp.py +++ /dev/null @@ -1,159 +0,0 @@ -import collections -import json - -import six -from ayon_api.graphql import GraphQlQuery, FIELD_VALUE, fields_to_dict - -from .constants import DEFAULT_FOLDER_FIELDS - - -def folders_tasks_graphql_query(fields): - query = GraphQlQuery("FoldersQuery") - project_name_var = query.add_variable("projectName", "String!") - folder_ids_var = query.add_variable("folderIds", "[String!]") - parent_folder_ids_var = query.add_variable("parentFolderIds", "[String!]") - folder_paths_var = query.add_variable("folderPaths", "[String!]") - folder_names_var = query.add_variable("folderNames", "[String!]") - has_products_var = query.add_variable("folderHasProducts", "Boolean!") - - project_field = query.add_field("project") - project_field.set_filter("name", project_name_var) - - folders_field = project_field.add_field_with_edges("folders") - folders_field.set_filter("ids", folder_ids_var) - folders_field.set_filter("parentIds", parent_folder_ids_var) - folders_field.set_filter("names", folder_names_var) - folders_field.set_filter("paths", folder_paths_var) - folders_field.set_filter("hasProducts", has_products_var) - - fields = set(fields) - fields.discard("tasks") - tasks_field = folders_field.add_field_with_edges("tasks") - tasks_field.add_field("name") - tasks_field.add_field("taskType") - - nested_fields = fields_to_dict(fields) - - query_queue = collections.deque() - for key, value in nested_fields.items(): - query_queue.append((key, value, folders_field)) - - while query_queue: - item = query_queue.popleft() - key, value, parent = item - field = parent.add_field(key) - if value is FIELD_VALUE: - continue - - for k, v in value.items(): - query_queue.append((k, v, field)) - return query - - -def get_folders_with_tasks( - con, - project_name, - folder_ids=None, - folder_paths=None, - folder_names=None, - parent_ids=None, - active=True, - fields=None -): - """Query folders with tasks from server. - - This is for v4 compatibility where tasks were stored on assets. This is - an inefficient way how folders and tasks are queried so it was added only - as compatibility function. - - Todos: - Folder name won't be unique identifier, so we should add folder path - filtering. - - Notes: - Filter 'active' don't have direct filter in GraphQl. - - Args: - con (ServerAPI): Connection to server. - project_name (str): Name of project where folders are. - folder_ids (Iterable[str]): Folder ids to filter. - folder_paths (Iterable[str]): Folder paths used for filtering. - folder_names (Iterable[str]): Folder names used for filtering. - parent_ids (Iterable[str]): Ids of folder parents. Use 'None' - if folder is direct child of project. - active (Union[bool, None]): Filter active/inactive folders. Both - are returned if is set to None. - fields (Union[Iterable(str), None]): Fields to be queried - for folder. All possible folder fields are returned if 'None' - is passed. - - Yields: - Dict[str, Any]: Queried folder entities. - """ - - if not project_name: - return - - filters = { - "projectName": project_name - } - if folder_ids is not None: - folder_ids = set(folder_ids) - if not folder_ids: - return - filters["folderIds"] = list(folder_ids) - - if folder_paths is not None: - folder_paths = set(folder_paths) - if not folder_paths: - return - filters["folderPaths"] = list(folder_paths) - - if folder_names is not None: - folder_names = set(folder_names) - if not folder_names: - return - filters["folderNames"] = list(folder_names) - - if parent_ids is not None: - parent_ids = set(parent_ids) - if not parent_ids: - return - if None in parent_ids: - # Replace 'None' with '"root"' which is used during GraphQl - # query for parent ids filter for folders without folder - # parent - parent_ids.remove(None) - parent_ids.add("root") - - if project_name in parent_ids: - # Replace project name with '"root"' which is used during - # GraphQl query for parent ids filter for folders without - # folder parent - parent_ids.remove(project_name) - parent_ids.add("root") - - filters["parentFolderIds"] = list(parent_ids) - - if fields: - fields = set(fields) - else: - fields = con.get_default_fields_for_type("folder") - fields |= DEFAULT_FOLDER_FIELDS - - if active is not None: - fields.add("active") - - query = folders_tasks_graphql_query(fields) - for attr, filter_value in filters.items(): - query.set_variable_value(attr, filter_value) - - parsed_data = query.query(con) - folders = parsed_data["project"]["folders"] - for folder in folders: - if active is not None and folder["active"] is not active: - continue - folder_data = folder.get("data") - if isinstance(folder_data, six.string_types): - folder["data"] = json.loads(folder_data) - yield folder diff --git a/client/ayon_core/client/operations.py b/client/ayon_core/client/operations.py deleted file mode 100644 index 71b3ca226a..0000000000 --- a/client/ayon_core/client/operations.py +++ /dev/null @@ -1,880 +0,0 @@ -import copy -import json -import collections -import uuid -import datetime - -from ayon_api.server_api import ( - PROJECT_NAME_ALLOWED_SYMBOLS, - PROJECT_NAME_REGEX, -) - -from .constants import ( - CURRENT_PROJECT_SCHEMA, - CURRENT_PROJECT_CONFIG_SCHEMA, - CURRENT_ASSET_DOC_SCHEMA, - CURRENT_SUBSET_SCHEMA, - CURRENT_VERSION_SCHEMA, - CURRENT_HERO_VERSION_SCHEMA, - CURRENT_REPRESENTATION_SCHEMA, - CURRENT_WORKFILE_INFO_SCHEMA, - CURRENT_THUMBNAIL_SCHEMA, -) -from .operations_base import ( - REMOVED_VALUE, - CreateOperation, - UpdateOperation, - DeleteOperation, - BaseOperationsSession -) -from .conversion_utils import ( - convert_create_asset_to_v4, - convert_create_task_to_v4, - convert_create_subset_to_v4, - convert_create_version_to_v4, - convert_create_hero_version_to_v4, - convert_create_representation_to_v4, - convert_create_workfile_info_to_v4, - - convert_update_folder_to_v4, - convert_update_subset_to_v4, - convert_update_version_to_v4, - convert_update_hero_version_to_v4, - convert_update_representation_to_v4, - convert_update_workfile_info_to_v4, -) -from .utils import create_entity_id, get_ayon_server_api_connection - - -def _create_or_convert_to_id(entity_id=None): - if entity_id is None: - return create_entity_id() - - # Validate if can be converted to uuid - uuid.UUID(entity_id) - return entity_id - - -def new_project_document( - project_name, project_code, config, data=None, entity_id=None -): - """Create skeleton data of project document. - - Args: - project_name (str): Name of project. Used as identifier of a project. - project_code (str): Shorter version of projet without spaces and - special characters (in most of cases). Should be also considered - as unique name across projects. - config (Dic[str, Any]): Project config consist of roots, templates, - applications and other project Anatomy related data. - data (Dict[str, Any]): Project data with information about it's - attributes (e.g. 'fps' etc.) or integration specific keys. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of project document. - """ - - if data is None: - data = {} - - data["code"] = project_code - - return { - "_id": _create_or_convert_to_id(entity_id), - "name": project_name, - "type": CURRENT_PROJECT_SCHEMA, - "entity_data": data, - "config": config - } - - -def new_asset_document( - name, project_id, parent_id, parents, data=None, entity_id=None -): - """Create skeleton data of asset document. - - Args: - name (str): Is considered as unique identifier of asset in project. - project_id (Union[str, ObjectId]): Id of project doument. - parent_id (Union[str, ObjectId]): Id of parent asset. - parents (List[str]): List of parent assets names. - data (Dict[str, Any]): Asset document data. Empty dictionary is used - if not passed. Value of 'parent_id' is used to fill 'visualParent'. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of asset document. - """ - - if data is None: - data = {} - if parent_id is not None: - parent_id = _create_or_convert_to_id(parent_id) - data["visualParent"] = parent_id - data["parents"] = parents - - return { - "_id": _create_or_convert_to_id(entity_id), - "type": "asset", - "name": name, - # This will be ignored - "parent": project_id, - "data": data, - "schema": CURRENT_ASSET_DOC_SCHEMA - } - - -def new_subset_document(name, family, asset_id, data=None, entity_id=None): - """Create skeleton data of subset document. - - Args: - name (str): Is considered as unique identifier of subset under asset. - family (str): Subset's family. - asset_id (Union[str, ObjectId]): Id of parent asset. - data (Dict[str, Any]): Subset document data. Empty dictionary is used - if not passed. Value of 'family' is used to fill 'family'. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of subset document. - """ - - if data is None: - data = {} - data["family"] = family - return { - "_id": _create_or_convert_to_id(entity_id), - "schema": CURRENT_SUBSET_SCHEMA, - "type": "subset", - "name": name, - "data": data, - "parent": _create_or_convert_to_id(asset_id) - } - - -def new_version_doc(version, subset_id, data=None, entity_id=None): - """Create skeleton data of version document. - - Args: - version (int): Is considered as unique identifier of version - under subset. - subset_id (Union[str, ObjectId]): Id of parent subset. - data (Dict[str, Any]): Version document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of version document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_id(entity_id), - "schema": CURRENT_VERSION_SCHEMA, - "type": "version", - "name": int(version), - "parent": _create_or_convert_to_id(subset_id), - "data": data - } - - -def new_hero_version_doc(subset_id, data, version=None, entity_id=None): - """Create skeleton data of hero version document. - - Args: - subset_id (Union[str, ObjectId]): Id of parent subset. - data (Dict[str, Any]): Version document data. - version (int): Version of source version. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of version document. - """ - - if version is None: - version = -1 - elif version > 0: - version = -version - - return { - "_id": _create_or_convert_to_id(entity_id), - "schema": CURRENT_HERO_VERSION_SCHEMA, - "type": "hero_version", - "version": version, - "parent": _create_or_convert_to_id(subset_id), - "data": data - } - - -def new_representation_doc( - name, version_id, context, data=None, entity_id=None -): - """Create skeleton data of representation document. - - Args: - name (str): Representation name considered as unique identifier - of representation under version. - version_id (Union[str, ObjectId]): Id of parent version. - context (Dict[str, Any]): Representation context used for fill template - of to query. - data (Dict[str, Any]): Representation document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of version document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_id(entity_id), - "schema": CURRENT_REPRESENTATION_SCHEMA, - "type": "representation", - "parent": _create_or_convert_to_id(version_id), - "name": name, - "data": data, - - # Imprint shortcut to context for performance reasons. - "context": context - } - - -def new_thumbnail_doc(data=None, entity_id=None): - """Create skeleton data of thumbnail document. - - Args: - data (Dict[str, Any]): Thumbnail document data. - entity_id (Union[str, ObjectId]): Predefined id of document. New id is - created if not passed. - - Returns: - Dict[str, Any]: Skeleton of thumbnail document. - """ - - if data is None: - data = {} - - return { - "_id": _create_or_convert_to_id(entity_id), - "type": "thumbnail", - "schema": CURRENT_THUMBNAIL_SCHEMA, - "data": data - } - - -def new_workfile_info_doc( - filename, asset_id, task_name, files, data=None, entity_id=None -): - """Create skeleton data of workfile info document. - - Workfile document is at this moment used primarily for artist notes. - - Args: - filename (str): Filename of workfile. - asset_id (Union[str, ObjectId]): Id of asset under which workfile live. - task_name (str): Task under which was workfile created. - files (List[str]): List of rootless filepaths related to workfile. - data (Dict[str, Any]): Additional metadata. - - Returns: - Dict[str, Any]: Skeleton of workfile info document. - """ - - if not data: - data = {} - - return { - "_id": _create_or_convert_to_id(entity_id), - "type": "workfile", - "parent": _create_or_convert_to_id(asset_id), - "task_name": task_name, - "filename": filename, - "data": data, - "files": files - } - - -def _prepare_update_data(old_doc, new_doc, replace): - changes = {} - for key, value in new_doc.items(): - if key not in old_doc or value != old_doc[key]: - changes[key] = value - - if replace: - for key in old_doc.keys(): - if key not in new_doc: - changes[key] = REMOVED_VALUE - return changes - - -def prepare_subset_update_data(old_doc, new_doc, replace=True): - """Compare two subset documents and prepare update data. - - Based on compared values will create update data for - 'MongoUpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -def prepare_version_update_data(old_doc, new_doc, replace=True): - """Compare two version documents and prepare update data. - - Based on compared values will create update data for - 'MongoUpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -def prepare_hero_version_update_data(old_doc, new_doc, replace=True): - """Compare two hero version documents and prepare update data. - - Based on compared values will create update data for 'UpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - changes = _prepare_update_data(old_doc, new_doc, replace) - changes.pop("version_id", None) - return changes - - -def prepare_representation_update_data(old_doc, new_doc, replace=True): - """Compare two representation documents and prepare update data. - - Based on compared values will create update data for - 'MongoUpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - changes = _prepare_update_data(old_doc, new_doc, replace) - context = changes.get("data", {}).get("context") - # Make sure that both 'family' and 'subset' are in changes if - # one of them changed (they'll both become 'product'). - if ( - context - and ("family" in context or "subset" in context) - ): - context["family"] = new_doc["data"]["context"]["family"] - context["subset"] = new_doc["data"]["context"]["subset"] - - return changes - - -def prepare_workfile_info_update_data(old_doc, new_doc, replace=True): - """Compare two workfile info documents and prepare update data. - - Based on compared values will create update data for - 'MongoUpdateOperation'. - - Empty output means that documents are identical. - - Returns: - Dict[str, Any]: Changes between old and new document. - """ - - return _prepare_update_data(old_doc, new_doc, replace) - - -class FailedOperations(Exception): - pass - - -def entity_data_json_default(value): - if isinstance(value, datetime.datetime): - return int(value.timestamp()) - - raise TypeError( - "Object of type {} is not JSON serializable".format(str(type(value))) - ) - - -def failed_json_default(value): - return "< Failed value {} > {}".format(type(value), str(value)) - - -class ServerCreateOperation(CreateOperation): - """Operation to create an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - data (Dict[str, Any]): Data of entity that will be created. - """ - - def __init__(self, project_name, entity_type, data, session): - self._session = session - - if not data: - data = {} - data = copy.deepcopy(data) - if entity_type == "project": - raise ValueError("Project cannot be created using operations") - - tasks = None - if entity_type in "asset": - # TODO handle tasks - entity_type = "folder" - if "data" in data: - tasks = data["data"].get("tasks") - - project = self._session.get_project(project_name) - new_data = convert_create_asset_to_v4(data, project, self.con) - - elif entity_type == "task": - project = self._session.get_project(project_name) - new_data = convert_create_task_to_v4(data, project, self.con) - - elif entity_type == "subset": - new_data = convert_create_subset_to_v4(data, self.con) - entity_type = "product" - - elif entity_type == "version": - new_data = convert_create_version_to_v4(data, self.con) - - elif entity_type == "hero_version": - new_data = convert_create_hero_version_to_v4( - data, project_name, self.con - ) - entity_type = "version" - - elif entity_type in ("representation", "archived_representation"): - new_data = convert_create_representation_to_v4(data, self.con) - entity_type = "representation" - - elif entity_type == "workfile": - new_data = convert_create_workfile_info_to_v4( - data, project_name, self.con - ) - - else: - raise ValueError( - "Unhandled entity type \"{}\"".format(entity_type) - ) - - # Simple check if data can be dumped into json - # - should raise error on 'ObjectId' object - try: - new_data = json.loads( - json.dumps(new_data, default=entity_data_json_default) - ) - - except: - raise ValueError("Couldn't json parse body: {}".format( - json.dumps(new_data, default=failed_json_default) - )) - - super(ServerCreateOperation, self).__init__( - project_name, entity_type, new_data - ) - - if "id" not in self._data: - self._data["id"] = create_entity_id() - - if tasks: - copied_tasks = copy.deepcopy(tasks) - for task_name, task in copied_tasks.items(): - task["name"] = task_name - task["folderId"] = self._data["id"] - self.session.create_entity( - project_name, "task", task, nested_id=self.id - ) - - @property - def con(self): - return self.session.con - - @property - def session(self): - return self._session - - @property - def entity_id(self): - return self._data["id"] - - def to_server_operation(self): - return { - "id": self.id, - "type": "create", - "entityType": self.entity_type, - "entityId": self.entity_id, - "data": self._data - } - - -class ServerUpdateOperation(UpdateOperation): - """Operation to update an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - entity_id (Union[str, ObjectId]): Identifier of an entity. - update_data (Dict[str, Any]): Key -> value changes that will be set in - database. If value is set to 'REMOVED_VALUE' the key will be - removed. Only first level of dictionary is checked (on purpose). - """ - - def __init__( - self, project_name, entity_type, entity_id, update_data, session - ): - self._session = session - - update_data = copy.deepcopy(update_data) - if entity_type == "project": - raise ValueError("Project cannot be created using operations") - - if entity_type in ("asset", "archived_asset"): - new_update_data = convert_update_folder_to_v4( - project_name, entity_id, update_data, self.con - ) - entity_type = "folder" - - elif entity_type == "subset": - new_update_data = convert_update_subset_to_v4( - project_name, entity_id, update_data, self.con - ) - entity_type = "product" - - elif entity_type == "version": - new_update_data = convert_update_version_to_v4( - project_name, entity_id, update_data, self.con - ) - - elif entity_type == "hero_version": - new_update_data = convert_update_hero_version_to_v4( - project_name, entity_id, update_data, self.con - ) - entity_type = "version" - - elif entity_type in ("representation", "archived_representation"): - new_update_data = convert_update_representation_to_v4( - project_name, entity_id, update_data, self.con - ) - entity_type = "representation" - - elif entity_type == "workfile": - new_update_data = convert_update_workfile_info_to_v4( - project_name, entity_id, update_data, self.con - ) - - else: - raise ValueError( - "Unhandled entity type \"{}\"".format(entity_type) - ) - - try: - new_update_data = json.loads( - json.dumps(new_update_data, default=entity_data_json_default) - ) - - except: - raise ValueError("Couldn't json parse body: {}".format( - json.dumps(new_update_data, default=failed_json_default) - )) - - super(ServerUpdateOperation, self).__init__( - project_name, entity_type, entity_id, new_update_data - ) - - @property - def con(self): - return self.session.con - - @property - def session(self): - return self._session - - def to_server_operation(self): - if not self._update_data: - return None - - update_data = {} - for key, value in self._update_data.items(): - if value is REMOVED_VALUE: - value = None - update_data[key] = value - - return { - "id": self.id, - "type": "update", - "entityType": self.entity_type, - "entityId": self.entity_id, - "data": update_data - } - - -class ServerDeleteOperation(DeleteOperation): - """Operation to delete an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - entity_id (Union[str, ObjectId]): Entity id that will be removed. - """ - - def __init__(self, project_name, entity_type, entity_id, session): - self._session = session - - if entity_type == "asset": - entity_type = "folder" - - elif entity_type == "hero_version": - entity_type = "version" - - elif entity_type == "subset": - entity_type = "product" - - super(ServerDeleteOperation, self).__init__( - project_name, entity_type, entity_id - ) - - @property - def con(self): - return self.session.con - - @property - def session(self): - return self._session - - def to_server_operation(self): - return { - "id": self.id, - "type": self.operation_name, - "entityId": self.entity_id, - "entityType": self.entity_type, - } - - -class OperationsSession(BaseOperationsSession): - def __init__(self, con=None, *args, **kwargs): - super(OperationsSession, self).__init__(*args, **kwargs) - if con is None: - con = get_ayon_server_api_connection() - self._con = con - self._project_cache = {} - self._nested_operations = collections.defaultdict(list) - - @property - def con(self): - return self._con - - def get_project(self, project_name): - if project_name not in self._project_cache: - self._project_cache[project_name] = self.con.get_project( - project_name) - return copy.deepcopy(self._project_cache[project_name]) - - def commit(self): - """Commit session operations.""" - - operations, self._operations = self._operations, [] - if not operations: - return - - operations_by_project = collections.defaultdict(list) - for operation in operations: - operations_by_project[operation.project_name].append(operation) - - body_by_id = {} - results = [] - for project_name, operations in operations_by_project.items(): - operations_body = [] - for operation in operations: - body = operation.to_server_operation() - if body is not None: - try: - json.dumps(body) - except: - raise ValueError("Couldn't json parse body: {}".format( - json.dumps( - body, indent=4, default=failed_json_default - ) - )) - - body_by_id[operation.id] = body - operations_body.append(body) - - if operations_body: - result = self._con.post( - "projects/{}/operations".format(project_name), - operations=operations_body, - canFail=False - ) - results.append(result.data) - - for result in results: - if result.get("success"): - continue - - if "operations" not in result: - raise FailedOperations( - "Operation failed. Content: {}".format(str(result)) - ) - - for op_result in result["operations"]: - if not op_result["success"]: - operation_id = op_result["id"] - raise FailedOperations(( - "Operation \"{}\" failed with data:\n{}\nError: {}." - ).format( - operation_id, - json.dumps(body_by_id[operation_id], indent=4), - op_result.get("error", "unknown"), - )) - - def create_entity(self, project_name, entity_type, data, nested_id=None): - """Fast access to 'ServerCreateOperation'. - - Args: - project_name (str): On which project the creation happens. - entity_type (str): Which entity type will be created. - data (Dicst[str, Any]): Entity data. - nested_id (str): Id of other operation from which is triggered - operation -> Operations can trigger suboperations but they - must be added to operations list after it's parent is added. - - Returns: - ServerCreateOperation: Object of update operation. - """ - - operation = ServerCreateOperation( - project_name, entity_type, data, self - ) - - if nested_id: - self._nested_operations[nested_id].append(operation) - else: - self.add(operation) - if operation.id in self._nested_operations: - self.extend(self._nested_operations.pop(operation.id)) - - return operation - - def update_entity( - self, project_name, entity_type, entity_id, update_data, nested_id=None - ): - """Fast access to 'ServerUpdateOperation'. - - Returns: - ServerUpdateOperation: Object of update operation. - """ - - operation = ServerUpdateOperation( - project_name, entity_type, entity_id, update_data, self - ) - if nested_id: - self._nested_operations[nested_id].append(operation) - else: - self.add(operation) - if operation.id in self._nested_operations: - self.extend(self._nested_operations.pop(operation.id)) - return operation - - def delete_entity( - self, project_name, entity_type, entity_id, nested_id=None - ): - """Fast access to 'ServerDeleteOperation'. - - Returns: - ServerDeleteOperation: Object of delete operation. - """ - - operation = ServerDeleteOperation( - project_name, entity_type, entity_id, self - ) - if nested_id: - self._nested_operations[nested_id].append(operation) - else: - self.add(operation) - if operation.id in self._nested_operations: - self.extend(self._nested_operations.pop(operation.id)) - return operation - - -def create_project( - project_name, - project_code, - library_project=False, - preset_name=None, - con=None -): - """Create project using OpenPype settings. - - This project creation function is not validating project document on - creation. It is because project document is created blindly with only - minimum required information about project which is it's name, code, type - and schema. - - Entered project name must be unique and project must not exist yet. - - Note: - This function is here to be OP v4 ready but in v3 has more logic - to do. That's why inner imports are in the body. - - Args: - project_name (str): New project name. Should be unique. - project_code (str): Project's code should be unique too. - library_project (bool): Project is library project. - preset_name (str): Name of anatomy preset. Default is used if not - passed. - con (ServerAPI): Connection to server with logged user. - - Raises: - ValueError: When project name already exists in MongoDB. - - Returns: - dict: Created project document. - """ - - if con is None: - con = get_ayon_server_api_connection() - - return con.create_project( - project_name, - project_code, - library_project, - preset_name - ) - - -def delete_project(project_name, con=None): - if con is None: - con = get_ayon_server_api_connection() - - return con.delete_project(project_name) - - -def create_thumbnail(project_name, src_filepath, thumbnail_id=None, con=None): - if con is None: - con = get_ayon_server_api_connection() - return con.create_thumbnail(project_name, src_filepath, thumbnail_id) diff --git a/client/ayon_core/client/operations_base.py b/client/ayon_core/client/operations_base.py deleted file mode 100644 index 887b237b1c..0000000000 --- a/client/ayon_core/client/operations_base.py +++ /dev/null @@ -1,289 +0,0 @@ -import uuid -import copy -from abc import ABCMeta, abstractmethod, abstractproperty -import six - -REMOVED_VALUE = object() - - -@six.add_metaclass(ABCMeta) -class AbstractOperation(object): - """Base operation class. - - Operation represent a call into database. The call can create, change or - remove data. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - """ - - def __init__(self, project_name, entity_type): - self._project_name = project_name - self._entity_type = entity_type - self._id = str(uuid.uuid4()) - - @property - def project_name(self): - return self._project_name - - @property - def id(self): - """Identifier of operation.""" - - return self._id - - @property - def entity_type(self): - return self._entity_type - - @abstractproperty - def operation_name(self): - """Stringified type of operation.""" - - pass - - def to_data(self): - """Convert operation to data that can be converted to json or others. - - Warning: - Current state returns ObjectId objects which cannot be parsed by - json. - - Returns: - Dict[str, Any]: Description of operation. - """ - - return { - "id": self._id, - "entity_type": self.entity_type, - "project_name": self.project_name, - "operation": self.operation_name - } - - -class CreateOperation(AbstractOperation): - """Operation to create an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - data (Dict[str, Any]): Data of entity that will be created. - """ - - operation_name = "create" - - def __init__(self, project_name, entity_type, data): - super(CreateOperation, self).__init__(project_name, entity_type) - - if not data: - data = {} - else: - data = copy.deepcopy(dict(data)) - self._data = data - - def __setitem__(self, key, value): - self.set_value(key, value) - - def __getitem__(self, key): - return self.data[key] - - def set_value(self, key, value): - self.data[key] = value - - def get(self, key, *args, **kwargs): - return self.data.get(key, *args, **kwargs) - - @abstractproperty - def entity_id(self): - pass - - @property - def data(self): - return self._data - - def to_data(self): - output = super(CreateOperation, self).to_data() - output["data"] = copy.deepcopy(self.data) - return output - - -class UpdateOperation(AbstractOperation): - """Operation to update an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - entity_id (Union[str, ObjectId]): Identifier of an entity. - update_data (Dict[str, Any]): Key -> value changes that will be set in - database. If value is set to 'REMOVED_VALUE' the key will be - removed. Only first level of dictionary is checked (on purpose). - """ - - operation_name = "update" - - def __init__(self, project_name, entity_type, entity_id, update_data): - super(UpdateOperation, self).__init__(project_name, entity_type) - - self._entity_id = entity_id - self._update_data = update_data - - @property - def entity_id(self): - return self._entity_id - - @property - def update_data(self): - return self._update_data - - def to_data(self): - changes = {} - for key, value in self._update_data.items(): - if value is REMOVED_VALUE: - value = None - changes[key] = value - - output = super(UpdateOperation, self).to_data() - output.update({ - "entity_id": self.entity_id, - "changes": changes - }) - return output - - -class DeleteOperation(AbstractOperation): - """Operation to delete an entity. - - Args: - project_name (str): On which project operation will happen. - entity_type (str): Type of entity on which change happens. - e.g. 'asset', 'representation' etc. - entity_id (Union[str, ObjectId]): Entity id that will be removed. - """ - - operation_name = "delete" - - def __init__(self, project_name, entity_type, entity_id): - super(DeleteOperation, self).__init__(project_name, entity_type) - - self._entity_id = entity_id - - @property - def entity_id(self): - return self._entity_id - - def to_data(self): - output = super(DeleteOperation, self).to_data() - output["entity_id"] = self.entity_id - return output - - -class BaseOperationsSession(object): - """Session storing operations that should happen in an order. - - At this moment does not handle anything special can be considered as - stupid list of operations that will happen after each other. If creation - of same entity is there multiple times it's handled in any way and document - values are not validated. - """ - - def __init__(self): - self._operations = [] - - def __len__(self): - return len(self._operations) - - def add(self, operation): - """Add operation to be processed. - - Args: - operation (BaseOperation): Operation that should be processed. - """ - if not isinstance( - operation, - (CreateOperation, UpdateOperation, DeleteOperation) - ): - raise TypeError("Expected Operation object got {}".format( - str(type(operation)) - )) - - self._operations.append(operation) - - def append(self, operation): - """Add operation to be processed. - - Args: - operation (BaseOperation): Operation that should be processed. - """ - - self.add(operation) - - def extend(self, operations): - """Add operations to be processed. - - Args: - operations (List[BaseOperation]): Operations that should be - processed. - """ - - for operation in operations: - self.add(operation) - - def remove(self, operation): - """Remove operation.""" - - self._operations.remove(operation) - - def clear(self): - """Clear all registered operations.""" - - self._operations = [] - - def to_data(self): - return [ - operation.to_data() - for operation in self._operations - ] - - @abstractmethod - def commit(self): - """Commit session operations.""" - pass - - def create_entity(self, project_name, entity_type, data): - """Fast access to 'CreateOperation'. - - Returns: - CreateOperation: Object of update operation. - """ - - operation = CreateOperation(project_name, entity_type, data) - self.add(operation) - return operation - - def update_entity(self, project_name, entity_type, entity_id, update_data): - """Fast access to 'UpdateOperation'. - - Returns: - UpdateOperation: Object of update operation. - """ - - operation = UpdateOperation( - project_name, entity_type, entity_id, update_data - ) - self.add(operation) - return operation - - def delete_entity(self, project_name, entity_type, entity_id): - """Fast access to 'DeleteOperation'. - - Returns: - DeleteOperation: Object of delete operation. - """ - - operation = DeleteOperation(project_name, entity_type, entity_id) - self.add(operation) - return operation diff --git a/client/ayon_core/client/thumbnails.py b/client/ayon_core/client/thumbnails.py deleted file mode 100644 index dc649b9651..0000000000 --- a/client/ayon_core/client/thumbnails.py +++ /dev/null @@ -1,229 +0,0 @@ -"""Cache of thumbnails downloaded from AYON server. - -Thumbnails are cached to appdirs to predefined directory. - -This should be moved to thumbnails logic in pipeline but because it would -overflow OpenPype logic it's here for now. -""" - -import os -import time -import collections - -import appdirs - -FileInfo = collections.namedtuple( - "FileInfo", - ("path", "size", "modification_time") -) - - -class AYONThumbnailCache: - """Cache of thumbnails on local storage. - - Thumbnails are cached to appdirs to predefined directory. Each project has - own subfolder with thumbnails -> that's because each project has own - thumbnail id validation and file names are thumbnail ids with matching - extension. Extensions are predefined (.png and .jpeg). - - Cache has cleanup mechanism which is triggered on initialized by default. - - The cleanup has 2 levels: - 1. soft cleanup which remove all files that are older then 'days_alive' - 2. max size cleanup which remove all files until the thumbnails folder - contains less then 'max_filesize' - - this is time consuming so it's not triggered automatically - - Args: - cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails). - """ - - # Lifetime of thumbnails (in seconds) - # - default 3 days - days_alive = 3 - # Max size of thumbnail directory (in bytes) - # - default 2 Gb - max_filesize = 2 * 1024 * 1024 * 1024 - - def __init__(self, cleanup=True): - self._thumbnails_dir = None - self._days_alive_secs = self.days_alive * 24 * 60 * 60 - if cleanup: - self.cleanup() - - def get_thumbnails_dir(self): - """Root directory where thumbnails are stored. - - Returns: - str: Path to thumbnails root. - """ - - if self._thumbnails_dir is None: - # TODO use generic function - directory = appdirs.user_data_dir("AYON", "Ynput") - self._thumbnails_dir = os.path.join(directory, "thumbnails") - return self._thumbnails_dir - - thumbnails_dir = property(get_thumbnails_dir) - - def get_thumbnails_dir_file_info(self): - """Get information about all files in thumbnails directory. - - Returns: - List[FileInfo]: List of file information about all files. - """ - - thumbnails_dir = self.thumbnails_dir - files_info = [] - if not os.path.exists(thumbnails_dir): - return files_info - - for root, _, filenames in os.walk(thumbnails_dir): - for filename in filenames: - path = os.path.join(root, filename) - files_info.append(FileInfo( - path, os.path.getsize(path), os.path.getmtime(path) - )) - return files_info - - def get_thumbnails_dir_size(self, files_info=None): - """Got full size of thumbnail directory. - - Args: - files_info (List[FileInfo]): Prepared file information about - files in thumbnail directory. - - Returns: - int: File size of all files in thumbnail directory. - """ - - if files_info is None: - files_info = self.get_thumbnails_dir_file_info() - - if not files_info: - return 0 - - return sum( - file_info.size - for file_info in files_info - ) - - def cleanup(self, check_max_size=False): - """Cleanup thumbnails directory. - - Args: - check_max_size (bool): Also cleanup files to match max size of - thumbnails directory. - """ - - thumbnails_dir = self.get_thumbnails_dir() - # Skip if thumbnails dir does not exists yet - if not os.path.exists(thumbnails_dir): - return - - self._soft_cleanup(thumbnails_dir) - if check_max_size: - self._max_size_cleanup(thumbnails_dir) - - def _soft_cleanup(self, thumbnails_dir): - current_time = time.time() - for root, _, filenames in os.walk(thumbnails_dir): - for filename in filenames: - path = os.path.join(root, filename) - modification_time = os.path.getmtime(path) - if current_time - modification_time > self._days_alive_secs: - os.remove(path) - - def _max_size_cleanup(self, thumbnails_dir): - files_info = self.get_thumbnails_dir_file_info() - size = self.get_thumbnails_dir_size(files_info) - if size < self.max_filesize: - return - - sorted_file_info = collections.deque( - sorted(files_info, key=lambda item: item.modification_time) - ) - diff = size - self.max_filesize - while diff > 0: - if not sorted_file_info: - break - - file_info = sorted_file_info.popleft() - diff -= file_info.size - os.remove(file_info.path) - - def get_thumbnail_filepath(self, project_name, thumbnail_id): - """Get thumbnail by thumbnail id. - - Args: - project_name (str): Name of project. - thumbnail_id (str): Thumbnail id. - - Returns: - Union[str, None]: Path to thumbnail image or None if thumbnail - is not cached yet. - """ - - if not thumbnail_id: - return None - - for ext in ( - ".png", - ".jpeg", - ): - filepath = os.path.join( - self.thumbnails_dir, project_name, thumbnail_id + ext - ) - if os.path.exists(filepath): - return filepath - return None - - def get_project_dir(self, project_name): - """Path to root directory for specific project. - - Args: - project_name (str): Name of project for which root directory path - should be returned. - - Returns: - str: Path to root of project's thumbnails. - """ - - return os.path.join(self.thumbnails_dir, project_name) - - def make_sure_project_dir_exists(self, project_name): - project_dir = self.get_project_dir(project_name) - if not os.path.exists(project_dir): - os.makedirs(project_dir) - return project_dir - - def store_thumbnail(self, project_name, thumbnail_id, content, mime_type): - """Store thumbnail to cache folder. - - Args: - project_name (str): Project where the thumbnail belong to. - thumbnail_id (str): Id of thumbnail. - content (bytes): Byte content of thumbnail file. - mime_data (str): Type of content. - - Returns: - str: Path to cached thumbnail image file. - """ - - if mime_type == "image/png": - ext = ".png" - elif mime_type == "image/jpeg": - ext = ".jpeg" - else: - raise ValueError( - "Unknown mime type for thumbnail \"{}\"".format(mime_type)) - - project_dir = self.make_sure_project_dir_exists(project_name) - thumbnail_path = os.path.join(project_dir, thumbnail_id + ext) - with open(thumbnail_path, "wb") as stream: - stream.write(content) - - current_time = time.time() - os.utime(thumbnail_path, (current_time, current_time)) - - return thumbnail_path diff --git a/client/ayon_core/client/utils.py b/client/ayon_core/client/utils.py deleted file mode 100644 index 26da6e34e1..0000000000 --- a/client/ayon_core/client/utils.py +++ /dev/null @@ -1,134 +0,0 @@ -import os -import uuid - -import ayon_api - -from ayon_core.client.operations_base import REMOVED_VALUE - - -class _GlobalCache: - initialized = False - - -def get_ayon_server_api_connection(): - if _GlobalCache.initialized: - con = ayon_api.get_server_api_connection() - else: - from ayon_core.lib.local_settings import get_local_site_id - - _GlobalCache.initialized = True - site_id = get_local_site_id() - version = os.getenv("AYON_VERSION") - if ayon_api.is_connection_created(): - con = ayon_api.get_server_api_connection() - con.set_site_id(site_id) - con.set_client_version(version) - else: - con = ayon_api.create_connection(site_id, version) - return con - - -def create_entity_id(): - return uuid.uuid1().hex - - -def prepare_attribute_changes(old_entity, new_entity, replace=False): - """Prepare changes of attributes on entities. - - Compare 'attrib' of old and new entity data to prepare only changed - values that should be sent to server for update. - - Example: - >>> # Limited entity data to 'attrib' - >>> old_entity = { - ... "attrib": {"attr_1": 1, "attr_2": "MyString", "attr_3": True} - ... } - >>> new_entity = { - ... "attrib": {"attr_1": 2, "attr_3": True, "attr_4": 3} - ... } - >>> # Changes if replacement should not happen - >>> expected_changes = { - ... "attr_1": 2, - ... "attr_4": 3 - ... } - >>> changes = prepare_attribute_changes(old_entity, new_entity) - >>> changes == expected_changes - True - - >>> # Changes if replacement should happen - >>> expected_changes_replace = { - ... "attr_1": 2, - ... "attr_2": REMOVED_VALUE, - ... "attr_4": 3 - ... } - >>> changes_replace = prepare_attribute_changes( - ... old_entity, new_entity, True) - >>> changes_replace == expected_changes_replace - True - - Args: - old_entity (dict[str, Any]): Data of entity queried from server. - new_entity (dict[str, Any]): Entity data with applied changes. - replace (bool): New entity should fully replace all old entity values. - - Returns: - Dict[str, Any]: Values from new entity only if value has changed. - """ - - attrib_changes = {} - new_attrib = new_entity.get("attrib") - old_attrib = old_entity.get("attrib") - if new_attrib is None: - if not replace: - return attrib_changes - new_attrib = {} - - if old_attrib is None: - return new_attrib - - for attr, new_attr_value in new_attrib.items(): - old_attr_value = old_attrib.get(attr) - if old_attr_value != new_attr_value: - attrib_changes[attr] = new_attr_value - - if replace: - for attr in old_attrib: - if attr not in new_attrib: - attrib_changes[attr] = REMOVED_VALUE - - return attrib_changes - - -def prepare_entity_changes(old_entity, new_entity, replace=False): - """Prepare changes of AYON entities. - - Compare old and new entity to filter values from new data that changed. - - Args: - old_entity (dict[str, Any]): Data of entity queried from server. - new_entity (dict[str, Any]): Entity data with applied changes. - replace (bool): All attributes should be replaced by new values. So - all attribute values that are not on new entity will be removed. - - Returns: - Dict[str, Any]: Only values from new entity that changed. - """ - - changes = {} - for key, new_value in new_entity.items(): - if key == "attrib": - continue - - old_value = old_entity.get(key) - if old_value != new_value: - changes[key] = new_value - - if replace: - for key in old_entity: - if key not in new_entity: - changes[key] = REMOVED_VALUE - - attr_changes = prepare_attribute_changes(old_entity, new_entity, replace) - if attr_changes: - changes["attrib"] = attr_changes - return changes diff --git a/client/ayon_core/hooks/pre_copy_template_workfile.py b/client/ayon_core/hooks/pre_copy_template_workfile.py index df2a0386b2..096ad7dd7e 100644 --- a/client/ayon_core/hooks/pre_copy_template_workfile.py +++ b/client/ayon_core/hooks/pre_copy_template_workfile.py @@ -54,21 +54,22 @@ class CopyTemplateWorkfile(PreLaunchHook): self.log.info("Last workfile does not exist.") project_name = self.data["project_name"] - asset_name = self.data["folder_path"] + folder_path = self.data["folder_path"] task_name = self.data["task_name"] host_name = self.application.host_name project_settings = get_project_settings(project_name) - project_doc = self.data.get("project_doc") - asset_doc = self.data.get("asset_doc") + project_entity = self.data.get("project_entity") + folder_entity = self.data.get("folder_entity") + task_entity = self.data.get("task_entity") anatomy = self.data.get("anatomy") - if project_doc and asset_doc: + if project_entity and folder_entity and task_entity: self.log.debug("Started filtering of custom template paths.") template_path = get_custom_workfile_template( - project_doc, - asset_doc, - task_name, + project_entity, + folder_entity, + task_entity, host_name, anatomy, project_settings @@ -81,7 +82,7 @@ class CopyTemplateWorkfile(PreLaunchHook): )) template_path = get_custom_workfile_template_by_string_context( project_name, - asset_name, + folder_path, task_name, host_name, anatomy, diff --git a/client/ayon_core/hooks/pre_global_host_data.py b/client/ayon_core/hooks/pre_global_host_data.py index de6d4acc8b..27e66450ab 100644 --- a/client/ayon_core/hooks/pre_global_host_data.py +++ b/client/ayon_core/hooks/pre_global_host_data.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_project, get_asset_by_name +from ayon_api import get_project, get_folder_by_path, get_task_by_name + from ayon_core.lib.applications import ( PreLaunchHook, EnvironmentPrepData, @@ -16,7 +17,7 @@ class GlobalHostDataHook(PreLaunchHook): """Prepare global objects to `data` that will be used for sure.""" self.prepare_global_data() - if not self.data.get("asset_doc"): + if not self.data.get("folder_entity"): return app = self.launch_context.application @@ -27,8 +28,9 @@ class GlobalHostDataHook(PreLaunchHook): "app": app, - "project_doc": self.data["project_doc"], - "asset_doc": self.data["asset_doc"], + "project_entity": self.data["project_entity"], + "folder_entity": self.data["folder_entity"], + "task_entity": self.data["task_entity"], "anatomy": self.data["anatomy"], @@ -59,19 +61,37 @@ class GlobalHostDataHook(PreLaunchHook): return self.log.debug("Project name is set to \"{}\"".format(project_name)) + + # Project Entity + project_entity = get_project(project_name) + self.data["project_entity"] = project_entity + # Anatomy - self.data["anatomy"] = Anatomy(project_name) + self.data["anatomy"] = Anatomy( + project_name, project_entity=project_entity + ) - # Project document - project_doc = get_project(project_name) - self.data["project_doc"] = project_doc - - asset_name = self.data.get("folder_path") - if not asset_name: + folder_path = self.data.get("folder_path") + if not folder_path: self.log.warning( - "Asset name was not set. Skipping asset document query." + "Folder path is not set. Skipping folder query." ) return - asset_doc = get_asset_by_name(project_name, asset_name) - self.data["asset_doc"] = asset_doc + folder_entity = get_folder_by_path(project_name, folder_path) + self.data["folder_entity"] = folder_entity + + task_name = self.data.get("task_name") + if not task_name: + self.log.warning( + "Task name is not set. Skipping task query." + ) + return + + if not folder_entity: + return + + task_entity = get_task_by_name( + project_name, folder_entity["id"], task_name + ) + self.data["task_entity"] = task_entity \ No newline at end of file diff --git a/client/ayon_core/hooks/pre_ocio_hook.py b/client/ayon_core/hooks/pre_ocio_hook.py index 08d9563975..e135a5bb12 100644 --- a/client/ayon_core/hooks/pre_ocio_hook.py +++ b/client/ayon_core/hooks/pre_ocio_hook.py @@ -28,7 +28,7 @@ class OCIOEnvHook(PreLaunchHook): template_data = get_template_data_with_names( project_name=self.data["project_name"], - asset_name=self.data["folder_path"], + folder_path=self.data["folder_path"], task_name=self.data["task_name"], host_name=self.host_name, settings=self.data["project_settings"] diff --git a/client/ayon_core/host/host.py b/client/ayon_core/host/host.py index f79c22824b..081aafdbe3 100644 --- a/client/ayon_core/host/host.py +++ b/client/ayon_core/host/host.py @@ -18,7 +18,7 @@ class HostBase(object): Compared to 'avalon' concept: What was before considered as functions in host implementation folder. The host implementation should primarily care about adding ability of creation - (mark subsets to be published) and optionally about referencing published + (mark products to be published) and optionally about referencing published representations as containers. Host may need extend some functionality like working with workfiles @@ -108,7 +108,7 @@ class HostBase(object): return os.environ.get("AYON_PROJECT_NAME") - def get_current_asset_name(self): + def get_current_folder_path(self): """ Returns: Union[str, None]: Current asset name. @@ -139,7 +139,7 @@ class HostBase(object): return { "project_name": self.get_current_project_name(), - "folder_path": self.get_current_asset_name(), + "folder_path": self.get_current_folder_path(), "task_name": self.get_current_task_name() } @@ -161,13 +161,13 @@ class HostBase(object): # Use current context to fill the context title current_context = self.get_current_context() project_name = current_context["project_name"] - asset_name = current_context["folder_path"] + folder_path = current_context["folder_path"] task_name = current_context["task_name"] items = [] if project_name: items.append(project_name) - if asset_name: - items.append(asset_name.lstrip("/")) + if folder_path: + items.append(folder_path.lstrip("/")) if task_name: items.append(task_name) if items: diff --git a/client/ayon_core/hosts/aftereffects/api/__init__.py b/client/ayon_core/hosts/aftereffects/api/__init__.py index 28062cc35d..4c4a8cce2f 100644 --- a/client/ayon_core/hosts/aftereffects/api/__init__.py +++ b/client/ayon_core/hosts/aftereffects/api/__init__.py @@ -17,7 +17,7 @@ from .pipeline import ( from .lib import ( maintained_selection, get_extension_manifest_path, - get_asset_settings, + get_folder_settings, set_settings ) @@ -37,7 +37,7 @@ __all__ = [ # lib "maintained_selection", "get_extension_manifest_path", - "get_asset_settings", + "get_folder_settings", "set_settings", # plugin diff --git a/client/ayon_core/hosts/aftereffects/api/launch_logic.py b/client/ayon_core/hosts/aftereffects/api/launch_logic.py index 0d1a6cf585..d0e4e8beae 100644 --- a/client/ayon_core/hosts/aftereffects/api/launch_logic.py +++ b/client/ayon_core/hosts/aftereffects/api/launch_logic.py @@ -286,20 +286,21 @@ class AfterEffectsRoute(WebSocketRoute): # This method calls function on the client side # client functions - async def set_context(self, project, asset, task): + async def set_context(self, project, folder, task): """ - Sets 'project' and 'asset' to envs, eg. setting context + Sets 'project', 'folder' and 'task' to envs, eg. setting context Args: project (str) - asset (str) + folder (str) + task (str) """ log.info("Setting context change") - log.info("project {} asset {} ".format(project, asset)) + log.info("project {} folder {} ".format(project, folder)) if project: os.environ["AYON_PROJECT_NAME"] = project - if asset: - os.environ["AYON_FOLDER_PATH"] = asset + if folder: + os.environ["AYON_FOLDER_PATH"] = folder if task: os.environ["AYON_TASK_NAME"] = task diff --git a/client/ayon_core/hosts/aftereffects/api/lib.py b/client/ayon_core/hosts/aftereffects/api/lib.py index 0a2ee7b7ac..d476378dcd 100644 --- a/client/ayon_core/hosts/aftereffects/api/lib.py +++ b/client/ayon_core/hosts/aftereffects/api/lib.py @@ -4,8 +4,10 @@ import json import contextlib import logging +import ayon_api + from ayon_core.pipeline.context_tools import get_current_context -from ayon_core.client import get_asset_by_name + from .ws_stub import get_stub log = logging.getLogger(__name__) @@ -85,21 +87,21 @@ def get_background_layers(file_url): return layers -def get_asset_settings(asset_doc): - """Get settings on current asset from database. +def get_folder_settings(folder_entity): + """Get settings of current folder. Returns: dict: Scene data. """ - asset_data = asset_doc["data"] - fps = asset_data.get("fps", 0) - frame_start = asset_data.get("frameStart", 0) - frame_end = asset_data.get("frameEnd", 0) - handle_start = asset_data.get("handleStart", 0) - handle_end = asset_data.get("handleEnd", 0) - resolution_width = asset_data.get("resolutionWidth", 0) - resolution_height = asset_data.get("resolutionHeight", 0) + folder_attributes = folder_entity["attrib"] + fps = folder_attributes.get("fps", 0) + frame_start = folder_attributes.get("frameStart", 0) + frame_end = folder_attributes.get("frameEnd", 0) + handle_start = folder_attributes.get("handleStart", 0) + handle_end = folder_attributes.get("handleEnd", 0) + resolution_width = folder_attributes.get("resolutionWidth", 0) + resolution_height = folder_attributes.get("resolutionHeight", 0) duration = (frame_end - frame_start + 1) + handle_start + handle_end return { @@ -127,9 +129,11 @@ def set_settings(frames, resolution, comp_ids=None, print_msg=True): frame_start = frames_duration = fps = width = height = None current_context = get_current_context() - asset_doc = get_asset_by_name(current_context["project_name"], - current_context["folder_path"]) - settings = get_asset_settings(asset_doc) + folder_entity = ayon_api.get_folder_by_path( + current_context["project_name"], + current_context["folder_path"] + ) + settings = get_folder_settings(folder_entity) msg = '' if frames: diff --git a/client/ayon_core/hosts/aftereffects/api/pipeline.py b/client/ayon_core/hosts/aftereffects/api/pipeline.py index 7ed244fd1d..105fee64b9 100644 --- a/client/ayon_core/hosts/aftereffects/api/pipeline.py +++ b/client/ayon_core/hosts/aftereffects/api/pipeline.py @@ -271,7 +271,7 @@ def containerise(name, "name": name, "namespace": namespace, "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "members": comp.members or [comp.id] } diff --git a/client/ayon_core/hosts/aftereffects/plugins/create/create_render.py b/client/ayon_core/hosts/aftereffects/plugins/create/create_render.py index 93aec33222..29df34876a 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/create/create_render.py +++ b/client/ayon_core/hosts/aftereffects/plugins/create/create_render.py @@ -218,7 +218,13 @@ class RenderCreator(Creator): """ def get_dynamic_data( - self, project_name, asset_doc, task_name, variant, host_name, instance + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ): dynamic_data = {} if instance is not None: diff --git a/client/ayon_core/hosts/aftereffects/plugins/create/workfile_creator.py b/client/ayon_core/hosts/aftereffects/plugins/create/workfile_creator.py index 282e06d0bf..b46e82bf1a 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/create/workfile_creator.py +++ b/client/ayon_core/hosts/aftereffects/plugins/create/workfile_creator.py @@ -1,5 +1,6 @@ +import ayon_api + import ayon_core.hosts.aftereffects.api as api -from ayon_core.client import get_asset_by_name from ayon_core.pipeline import ( AutoCreator, CreatedInstance @@ -39,32 +40,37 @@ class AEWorkfileCreator(AutoCreator): context = self.create_context project_name = context.get_current_project_name() - asset_name = context.get_current_asset_name() + folder_path = context.get_current_folder_path() task_name = context.get_current_task_name() host_name = context.host_name - existing_asset_name = None + existing_folder_path = None if existing_instance is not None: - existing_asset_name = existing_instance.get("folderPath") + existing_folder_path = existing_instance.get("folderPath") if existing_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant, } data.update(self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, None, @@ -79,17 +85,22 @@ class AEWorkfileCreator(AutoCreator): new_instance.data_to_store()) elif ( - existing_asset_name != asset_name + existing_folder_path != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/aftereffects/plugins/load/load_background.py b/client/ayon_core/hosts/aftereffects/plugins/load/load_background.py index b834875e89..e45de5524e 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/load/load_background.py +++ b/client/ayon_core/hosts/aftereffects/plugins/load/load_background.py @@ -31,7 +31,7 @@ class BackgroundLoader(api.AfterEffectsLoader): comp_name = get_unique_layer_name( existing_items, - "{}_{}".format(context["asset"]["name"], name)) + "{}_{}".format(context["folder"]["name"], name)) path = self.filepath_from_context(context) layers = get_background_layers(path) @@ -59,12 +59,10 @@ class BackgroundLoader(api.AfterEffectsLoader): def update(self, container, context): """ Switch asset or change version """ stub = self.get_stub() - asset_doc = context["asset"] - subset_doc = context["subset"] - repre_doc = context["representation"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + repre_entity = context["representation"] - folder_name = asset_doc["name"] - product_name = subset_doc["name"] _ = container.pop("layer") # without iterator number (_001, 002...) @@ -82,7 +80,7 @@ class BackgroundLoader(api.AfterEffectsLoader): else: # switching version - keep same name comp_name = container["namespace"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) layers = get_background_layers(path) comp = stub.reload_background(container["members"][1], @@ -90,7 +88,7 @@ class BackgroundLoader(api.AfterEffectsLoader): layers) # update container - container["representation"] = str(repre_doc["_id"]) + container["representation"] = repre_entity["id"] container["name"] = product_name container["namespace"] = comp_name container["members"] = comp.members diff --git a/client/ayon_core/hosts/aftereffects/plugins/load/load_file.py b/client/ayon_core/hosts/aftereffects/plugins/load/load_file.py index bceea66e8e..29536945dd 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/load/load_file.py +++ b/client/ayon_core/hosts/aftereffects/plugins/load/load_file.py @@ -25,7 +25,10 @@ class FileLoader(api.AfterEffectsLoader): layers = stub.get_items(comps=True, folders=True, footages=True) existing_layers = [layer.name for layer in layers] comp_name = get_unique_layer_name( - existing_layers, "{}_{}".format(context["asset"]["name"], name)) + existing_layers, "{}_{}".format( + context["folder"]["name"], name + ) + ) import_options = {} @@ -35,7 +38,7 @@ class FileLoader(api.AfterEffectsLoader): import_options['sequence'] = True if not path: - repr_id = context["representation"]["_id"] + repr_id = context["representation"]["id"] self.log.warning( "Representation id `{}` is failing to load".format(repr_id)) return @@ -69,12 +72,9 @@ class FileLoader(api.AfterEffectsLoader): stub = self.get_stub() layer = container.pop("layer") - asset_doc = context["asset"] - subset_doc = context["subset"] - repre_doc = context["representation"] - - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + repre_entity = context["representation"] namespace_from_container = re.sub(r'_\d{3}$', '', container["namespace"]) @@ -88,11 +88,11 @@ class FileLoader(api.AfterEffectsLoader): "{}_{}".format(folder_name, product_name)) else: # switching version - keep same name layer_name = container["namespace"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) # with aftereffects.maintained_selection(): # TODO stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name) stub.imprint( - layer.id, {"representation": str(repre_doc["_id"]), + layer.id, {"representation": repre_entity["id"], "name": product_name, "namespace": layer_name} ) diff --git a/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml b/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml index d89a851c64..23e1b50551 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml +++ b/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_instance_asset.xml @@ -1,7 +1,7 @@ -Subset context +Product context ## Invalid product context @@ -15,7 +15,7 @@ You can fix this with "repair" button on the right and refresh Publish at the bo ### __Detailed Info__ (optional) This might happen if you are reuse old workfile and open it in different context. -(Eg. you created product name "renderCompositingDefault" from folder "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing product for "Robot" asset stayed in the workfile.) +(Eg. you created product name "renderCompositingDefault" from folder "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing product for "Robot" folder stayed in the workfile.) \ No newline at end of file diff --git a/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml b/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml index 0591020ed3..b2da7af114 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml +++ b/client/ayon_core/hosts/aftereffects/plugins/publish/help/validate_scene_settings.xml @@ -5,20 +5,20 @@ ## Invalid scene setting found -One of the settings in a scene doesn't match to asset settings in database. +One of the settings in a scene doesn't match to folder settings in database. {invalid_setting_str} ### How to repair? -Change values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there. +Change values for {invalid_keys_str} in the scene OR change them in the folder database if they are wrong there. In the scene it is right mouse click on published composition > `Composition Settings`. ### __Detailed Info__ (optional) -This error is shown when for example resolution in the scene doesn't match to resolution set on the asset in the database. +This error is shown when for example resolution in the scene doesn't match to resolution set on the folder in the database. Either value in the database or in the scene is wrong. diff --git a/client/ayon_core/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/client/ayon_core/hosts/aftereffects/plugins/publish/validate_instance_asset.py index e8f2e29a2f..c4411bd4c2 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/publish/validate_instance_asset.py +++ b/client/ayon_core/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -1,6 +1,6 @@ import pyblish.api -from ayon_core.pipeline import get_current_asset_name +from ayon_core.pipeline import get_current_folder_path from ayon_core.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -8,8 +8,8 @@ from ayon_core.pipeline.publish import ( from ayon_core.hosts.aftereffects.api import get_stub -class ValidateInstanceAssetRepair(pyblish.api.Action): - """Repair the instance asset with value from Context.""" +class ValidateInstanceFolderRepair(pyblish.api.Action): + """Repair the instance folder with value from Context.""" label = "Repair" icon = "wrench" @@ -30,35 +30,35 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): for instance in instances: data = stub.read(instance[0]) - data["folderPath"] = get_current_asset_name() + data["folderPath"] = get_current_folder_path() stub.imprint(instance[0].instance_id, data) -class ValidateInstanceAsset(pyblish.api.InstancePlugin): - """Validate the instance asset is the current selected context asset. +class ValidateInstanceFolder(pyblish.api.InstancePlugin): + """Validate the instance folder is the current selected context folder. As it might happen that multiple worfiles are opened at same time, switching between them would mess with selected context. (From Launcher or Ftrack). - In that case outputs might be output under wrong asset! + In that case outputs might be output under wrong folder! - Repair action will use Context asset value (from Workfiles or Launcher) + Repair action will use Context folder value (from Workfiles or Launcher) Closing and reopening with Workfiles will refresh Context value. """ - label = "Validate Instance Asset" + label = "Validate Instance Folder" hosts = ["aftereffects"] - actions = [ValidateInstanceAssetRepair] + actions = [ValidateInstanceFolderRepair] order = ValidateContentsOrder def process(self, instance): - instance_asset = instance.data["folderPath"] - current_asset = get_current_asset_name() + instance_folder = instance.data["folderPath"] + current_folder = get_current_folder_path() msg = ( - f"Instance asset {instance_asset} is not the same " - f"as current context {current_asset}." + f"Instance folder {instance_folder} is not the same " + f"as current context {current_folder}." ) - if instance_asset != current_asset: + if instance_folder != current_folder: raise PublishXmlValidationError(self, msg) diff --git a/client/ayon_core/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/client/ayon_core/hosts/aftereffects/plugins/publish/validate_scene_settings.py index 0a90ae2a5a..6375f5cc61 100644 --- a/client/ayon_core/hosts/aftereffects/plugins/publish/validate_scene_settings.py +++ b/client/ayon_core/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Validate scene settings. Requires: - instance -> assetEntity + instance -> folderEntity instance -> anatomyData """ import os @@ -13,7 +13,7 @@ from ayon_core.pipeline import ( PublishXmlValidationError, OptionalPyblishPluginMixin ) -from ayon_core.hosts.aftereffects.api import get_asset_settings +from ayon_core.hosts.aftereffects.api import get_folder_settings class ValidateSceneSettings(OptionalPyblishPluginMixin, @@ -48,7 +48,7 @@ class ValidateSceneSettings(OptionalPyblishPluginMixin, fps handleStart handleEnd - skip_resolution_check - fill entity type ('asset') to skip validation + skip_resolution_check - fill entity type ('folder') to skip validation resolutionWidth resolutionHeight TODO support in extension is missing for now @@ -71,11 +71,11 @@ class ValidateSceneSettings(OptionalPyblishPluginMixin, if not self.is_active(instance.data): return - asset_doc = instance.data["assetEntity"] - expected_settings = get_asset_settings(asset_doc) + folder_entity = instance.data["folderEntity"] + expected_settings = get_folder_settings(folder_entity) self.log.info("config from DB::{}".format(expected_settings)) - task_name = instance.data["anatomyData"]["task"]["name"] + task_name = instance.data["task"] if any(re.search(pattern, task_name) for pattern in self.skip_resolution_check): expected_settings.pop("resolutionWidth") diff --git a/client/ayon_core/hosts/blender/api/ops.py b/client/ayon_core/hosts/blender/api/ops.py index dcbc44bcad..d71ee6faf5 100644 --- a/client/ayon_core/hosts/blender/api/ops.py +++ b/client/ayon_core/hosts/blender/api/ops.py @@ -16,7 +16,7 @@ import bpy import bpy.utils.previews from ayon_core import style -from ayon_core.pipeline import get_current_asset_name, get_current_task_name +from ayon_core.pipeline import get_current_folder_path, get_current_task_name from ayon_core.tools.utils import host_tools from .workio import OpenFileCacher @@ -355,7 +355,7 @@ class SetFrameRange(bpy.types.Operator): bl_label = "Set Frame Range" def execute(self, context): - data = pipeline.get_asset_data() + data = pipeline.get_folder_attributes() pipeline.set_frame_range(data) return {"FINISHED"} @@ -365,7 +365,7 @@ class SetResolution(bpy.types.Operator): bl_label = "Set Resolution" def execute(self, context): - data = pipeline.get_asset_data() + data = pipeline.get_folder_attributes() pipeline.set_resolution(data) return {"FINISHED"} @@ -388,9 +388,9 @@ class TOPBAR_MT_avalon(bpy.types.Menu): else: pyblish_menu_icon_id = 0 - asset = get_current_asset_name() - task = get_current_task_name() - context_label = f"{asset}, {task}" + folder_path = get_current_folder_path() + task_name = get_current_task_name() + context_label = f"{folder_path}, {task_name}" context_label_item = layout.row() context_label_item.operator( LaunchWorkFiles.bl_idname, text=context_label diff --git a/client/ayon_core/hosts/blender/api/pipeline.py b/client/ayon_core/hosts/blender/api/pipeline.py index fcac285f74..84e78d0883 100644 --- a/client/ayon_core/hosts/blender/api/pipeline.py +++ b/client/ayon_core/hosts/blender/api/pipeline.py @@ -9,6 +9,7 @@ from . import lib from . import ops import pyblish.api +import ayon_api from ayon_core.host import ( HostBase, @@ -16,11 +17,10 @@ from ayon_core.host import ( IPublishHost, ILoadHost ) -from ayon_core.client import get_asset_by_name from ayon_core.pipeline import ( schema, get_current_project_name, - get_current_asset_name, + get_current_folder_path, register_loader_plugin_path, register_creator_plugin_path, deregister_loader_plugin_path, @@ -221,12 +221,12 @@ def message_window(title, message): _process_app_events() -def get_asset_data(): +def get_folder_attributes(): project_name = get_current_project_name() - asset_name = get_current_asset_name() - asset_doc = get_asset_by_name(project_name, asset_name) + folder_path = get_current_folder_path() + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) - return asset_doc.get("data") + return folder_entity["attrib"] def set_frame_range(data): @@ -279,7 +279,7 @@ def on_new(): set_resolution_startup = settings.get("set_resolution_startup") set_frames_startup = settings.get("set_frames_startup") - data = get_asset_data() + data = get_folder_attributes() if set_resolution_startup: set_resolution(data) @@ -300,7 +300,7 @@ def on_open(): set_resolution_startup = settings.get("set_resolution_startup") set_frames_startup = settings.get("set_frames_startup") - data = get_asset_data() + data = get_folder_attributes() if set_resolution_startup: set_resolution(data) @@ -468,7 +468,7 @@ def containerise(name: str, """ - node_name = f"{context['asset']['name']}_{name}" + node_name = f"{context['folder']['name']}_{name}" if namespace: node_name = f"{namespace}:{node_name}" if suffix: @@ -484,7 +484,7 @@ def containerise(name: str, "name": name, "namespace": namespace or '', "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } metadata_update(container, data) @@ -523,7 +523,7 @@ def containerise_existing( "name": name, "namespace": namespace or '', "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } metadata_update(container, data) diff --git a/client/ayon_core/hosts/blender/api/plugin.py b/client/ayon_core/hosts/blender/api/plugin.py index 4b45d8ffa3..6c9bfb6569 100644 --- a/client/ayon_core/hosts/blender/api/plugin.py +++ b/client/ayon_core/hosts/blender/api/plugin.py @@ -49,7 +49,7 @@ def prepare_scene_name( def get_unique_number( folder_name: str, product_name: str ) -> str: - """Return a unique number based on the asset name.""" + """Return a unique number based on the folder name.""" avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) if not avalon_container: return "01" @@ -220,9 +220,9 @@ class BaseCreator(Creator): Create new instance and store it. Args: - product_name(str): Subset name of created instance. - instance_data(dict): Instance base data. - pre_create_data(dict): Data based on pre creation attributes. + product_name (str): Product name of created instance. + instance_data (dict): Instance base data. + pre_create_data (dict): Data based on pre creation attributes. Those may affect how creator works. """ # Get Instance Container or create it if it does not exist @@ -232,9 +232,9 @@ class BaseCreator(Creator): bpy.context.scene.collection.children.link(instances) # Create asset group - asset_name = instance_data["folderPath"].split("/")[-1] + folder_name = instance_data["folderPath"].split("/")[-1] - name = prepare_scene_name(asset_name, product_name) + name = prepare_scene_name(folder_name, product_name) if self.create_as_asset_group: # Create instance as empty instance_node = bpy.data.objects.new(name=name, object_data=None) @@ -312,9 +312,9 @@ class BaseCreator(Creator): "productName" in changes.changed_keys or "folderPath" in changes.changed_keys ) and created_instance.product_type != "workfile": - asset_name = data["folderPath"].split("/")[-1] + folder_name = data["folderPath"].split("/")[-1] name = prepare_scene_name( - asset_name, data["productName"] + folder_name, data["productName"] ) node.name = name @@ -346,7 +346,7 @@ class BaseCreator(Creator): """Fill instance data with required items. Args: - product_name(str): Subset name of created instance. + product_name(str): Product name of created instance. instance_data(dict): Instance base data. instance_node(bpy.types.ID): Instance node in blender scene. """ @@ -465,8 +465,8 @@ class AssetLoader(LoaderPlugin): filepath = self.filepath_from_context(context) assert Path(filepath).exists(), f"{filepath} doesn't exist." - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] unique_number = get_unique_number( folder_name, product_name ) @@ -498,8 +498,8 @@ class AssetLoader(LoaderPlugin): # loader=self.__class__.__name__, # ) - # folder_name = context["asset"]["name"] - # product_name = context["subset"]["name"] + # folder_name = context["folder"]["name"] + # product_name = context["product"]["name"] # instance_name = prepare_scene_name( # folder_name, product_name, unique_number # ) + '_CON' diff --git a/client/ayon_core/hosts/blender/plugins/create/convert_legacy.py b/client/ayon_core/hosts/blender/plugins/create/convert_legacy.py index 65a5a4a9b6..613574eee0 100644 --- a/client/ayon_core/hosts/blender/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/blender/plugins/create/convert_legacy.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """Converter for legacy Houdini products.""" -from ayon_core.pipeline.create.creator_plugins import SubsetConvertorPlugin +from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin from ayon_core.hosts.blender.api.lib import imprint -class BlenderLegacyConvertor(SubsetConvertorPlugin): +class BlenderLegacyConvertor(ProductConvertorPlugin): """Find and convert any legacy products in the scene. This Converter will find all legacy products in the scene and will diff --git a/client/ayon_core/hosts/blender/plugins/create/create_workfile.py b/client/ayon_core/hosts/blender/plugins/create/create_workfile.py index ead3ed7749..296a03e317 100644 --- a/client/ayon_core/hosts/blender/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/blender/plugins/create/create_workfile.py @@ -1,7 +1,7 @@ import bpy +import ayon_api from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.client import get_asset_by_name from ayon_core.hosts.blender.api.plugin import BaseCreator from ayon_core.hosts.blender.api.pipeline import ( AVALON_PROPERTY, @@ -33,33 +33,38 @@ class CreateWorkfile(BaseCreator, AutoCreator): ) project_name = self.project_name - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - existing_asset_name = None + existing_folder_path = None if workfile_instance is not None: - existing_asset_name = workfile_instance.get("folderPath") + existing_folder_path = workfile_instance.get("folderPath") if not workfile_instance: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, task_name, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": task_name, } data.update( self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, task_name, host_name, workfile_instance, @@ -72,20 +77,25 @@ class CreateWorkfile(BaseCreator, AutoCreator): self._add_instance_to_context(workfile_instance) elif ( - existing_asset_name != asset_name + existing_folder_path != folder_path or workfile_instance["task"] != task_name ): # Update instance context if it's different - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) - workfile_instance["folderPath"] = asset_name + workfile_instance["folderPath"] = folder_path workfile_instance["task"] = task_name workfile_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/blender/plugins/load/import_workfile.py b/client/ayon_core/hosts/blender/plugins/load/import_workfile.py index 5a801da848..7144c132e8 100644 --- a/client/ayon_core/hosts/blender/plugins/load/import_workfile.py +++ b/client/ayon_core/hosts/blender/plugins/load/import_workfile.py @@ -4,8 +4,8 @@ from ayon_core.hosts.blender.api import plugin def append_workfile(context, fname, do_import): - folder_name = context['asset']['name'] - product_name = context['subset']['name'] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] group_name = plugin.prepare_scene_name(folder_name, product_name) diff --git a/client/ayon_core/hosts/blender/plugins/load/load_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_abc.py index 3fc879f9c8..7ae9936ed5 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_abc.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_abc.py @@ -134,8 +134,8 @@ class CacheModelLoader(plugin.AssetLoader): """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -161,17 +161,17 @@ class CacheModelLoader(plugin.AssetLoader): self._link_objects(objects, asset_group, containers, asset_group) - product_type = context["subset"]["data"]["family"] + product_type = context["product"]["productType"] asset_group[AVALON_PROPERTY] = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), + "parent": context["representation"]["versionId"], "productType": product_type, "objectName": group_name } @@ -191,16 +191,16 @@ class CacheModelLoader(plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - repre_doc = context["representation"] + repre_entity = context["representation"] object_name = container["objectName"] asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert asset_group, ( @@ -245,7 +245,7 @@ class CacheModelLoader(plugin.AssetLoader): asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) - metadata["representation"] = str(repre_doc["_id"]) + metadata["representation"] = repre_entity["id"] def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. diff --git a/client/ayon_core/hosts/blender/plugins/load/load_action.py b/client/ayon_core/hosts/blender/plugins/load/load_action.py index df7ffe439d..a079387b9f 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_action.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_action.py @@ -44,8 +44,8 @@ class BlendActionLoader(plugin.AssetLoader): """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] lib_container = plugin.prepare_scene_name(folder_name, product_name) container_name = plugin.prepare_scene_name( folder_name, product_name, namespace @@ -126,18 +126,18 @@ class BlendActionLoader(plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - repre_doc = context["representation"] + repre_entity = context["representation"] collection = bpy.data.collections.get( container["objectName"] ) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() logger.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert collection, ( @@ -241,7 +241,7 @@ class BlendActionLoader(plugin.AssetLoader): # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) - collection_metadata["representation"] = str(repre_doc["_id"]) + collection_metadata["representation"] = repre_entity["id"] bpy.ops.object.select_all(action='DESELECT') diff --git a/client/ayon_core/hosts/blender/plugins/load/load_audio.py b/client/ayon_core/hosts/blender/plugins/load/load_audio.py index 85d5277d40..de74bf95d3 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_audio.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_audio.py @@ -39,8 +39,8 @@ class AudioLoader(plugin.AssetLoader): options: Additional settings dictionary """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -83,11 +83,11 @@ class AudioLoader(plugin.AssetLoader): "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name, "audio": audio } @@ -105,15 +105,15 @@ class AudioLoader(plugin.AssetLoader): representation (openpype:representation-1.0): Representation to update, from `host.ls()`. """ - repre_doc = context["representation"] + repre_entity = context["representation"] object_name = container["objectName"] asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert asset_group, ( @@ -176,8 +176,8 @@ class AudioLoader(plugin.AssetLoader): window_manager.windows[-1].screen.areas[0].type = old_type metadata["libpath"] = str(libpath) - metadata["representation"] = str(repre_doc["_id"]) - metadata["parent"] = str(repre_doc["parent"]) + metadata["representation"] = repre_entity["id"] + metadata["parent"] = repre_entity["versionId"] metadata["audio"] = new_audio def exec_remove(self, container: Dict) -> bool: diff --git a/client/ayon_core/hosts/blender/plugins/load/load_blend.py b/client/ayon_core/hosts/blender/plugins/load/load_blend.py index fdae9c1b6b..465485ba86 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_blend.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_blend.py @@ -127,15 +127,15 @@ class BlendLoader(plugin.AssetLoader): options: Additional settings dictionary """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] try: - product_type = context["subset"]["data"]["family"] + product_type = context["product"]["productType"] except ValueError: product_type = "model" - representation = str(context["representation"]["_id"]) + representation = context["representation"]["id"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -162,11 +162,11 @@ class BlendLoader(plugin.AssetLoader): "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name, "members": members, } @@ -185,10 +185,10 @@ class BlendLoader(plugin.AssetLoader): """ Update the loaded asset. """ - repre_doc = context["representation"] + repre_entity = context["representation"] group_name = container["objectName"] asset_group = bpy.data.objects.get(group_name) - libpath = Path(get_representation_path(repre_doc)).as_posix() + libpath = Path(get_representation_path(repre_entity)).as_posix() assert asset_group, ( f"The asset is not loaded: {container['objectName']}" @@ -235,8 +235,8 @@ class BlendLoader(plugin.AssetLoader): new_data = { "libpath": libpath, - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]), + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], "members": members, } diff --git a/client/ayon_core/hosts/blender/plugins/load/load_blendscene.py b/client/ayon_core/hosts/blender/plugins/load/load_blendscene.py index 52ecdd6a0a..2c93b739f8 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_blendscene.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_blendscene.py @@ -82,11 +82,11 @@ class BlendSceneLoader(plugin.AssetLoader): options: Additional settings dictionary """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] try: - product_type = context["subset"]["data"]["family"] + product_type = context["product"]["productType"] except ValueError: product_type = "model" @@ -114,11 +114,11 @@ class BlendSceneLoader(plugin.AssetLoader): "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name, "members": members, } @@ -137,10 +137,10 @@ class BlendSceneLoader(plugin.AssetLoader): """ Update the loaded asset. """ - repre_doc = context["representation"] + repre_entity = context["representation"] group_name = container["objectName"] asset_group = bpy.data.collections.get(group_name) - libpath = Path(get_representation_path(repre_doc)).as_posix() + libpath = Path(get_representation_path(repre_entity)).as_posix() assert asset_group, ( f"The asset is not loaded: {container['objectName']}" @@ -202,8 +202,8 @@ class BlendSceneLoader(plugin.AssetLoader): new_data = { "libpath": libpath, - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]), + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], "members": members, } diff --git a/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py b/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py index da90f0b1ab..98a4e2c2ec 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_camera_abc.py @@ -84,8 +84,8 @@ class AbcCameraLoader(plugin.AssetLoader): libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -119,11 +119,11 @@ class AbcCameraLoader(plugin.AssetLoader): "name": name, "namespace": namespace or "", "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name, } @@ -142,16 +142,16 @@ class AbcCameraLoader(plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - repre_doc = context["representation"] + repre_entity = context["representation"] object_name = container["objectName"] asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert asset_group, ( @@ -186,7 +186,7 @@ class AbcCameraLoader(plugin.AssetLoader): asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) - metadata["representation"] = str(repre_doc["_id"]) + metadata["representation"] = repre_entity["id"] def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. diff --git a/client/ayon_core/hosts/blender/plugins/load/load_camera_fbx.py b/client/ayon_core/hosts/blender/plugins/load/load_camera_fbx.py index 2024356e70..f31e53270e 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_camera_fbx.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_camera_fbx.py @@ -87,8 +87,8 @@ class FbxCameraLoader(plugin.AssetLoader): options: Additional settings dictionary """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -122,11 +122,11 @@ class FbxCameraLoader(plugin.AssetLoader): "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name } @@ -145,16 +145,16 @@ class FbxCameraLoader(plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - repre_doc = context["representation"] + repre_entity = context["representation"] object_name = container["objectName"] asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert asset_group, ( @@ -196,7 +196,7 @@ class FbxCameraLoader(plugin.AssetLoader): asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) - metadata["representation"] = str(repre_doc["_id"]) + metadata["representation"] = repre_entity["id"] def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. diff --git a/client/ayon_core/hosts/blender/plugins/load/load_fbx.py b/client/ayon_core/hosts/blender/plugins/load/load_fbx.py index 7b4acfed9a..1eaab4e7b7 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_fbx.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_fbx.py @@ -131,8 +131,8 @@ class FbxModelLoader(plugin.AssetLoader): options: Additional settings dictionary """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -166,11 +166,11 @@ class FbxModelLoader(plugin.AssetLoader): "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name } @@ -189,16 +189,16 @@ class FbxModelLoader(plugin.AssetLoader): Warning: No nested collections are supported at the moment! """ - repre_doc = context["representation"] + repre_entity = context["representation"] object_name = container["objectName"] asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert asset_group, ( @@ -251,7 +251,7 @@ class FbxModelLoader(plugin.AssetLoader): asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) - metadata["representation"] = str(repre_doc["_id"]) + metadata["representation"] = repre_entity["id"] def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. diff --git a/client/ayon_core/hosts/blender/plugins/load/load_layout_json.py b/client/ayon_core/hosts/blender/plugins/load/load_layout_json.py index 84793775e5..f3d14ac018 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_layout_json.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_layout_json.py @@ -132,7 +132,7 @@ class JsonLayoutLoader(plugin.AssetLoader): # # name=f"{unique_number}_{product[name]}_animation", # asset=asset, # options={"useSelection": False} - # # data={"dependencies": str(context["representation"]["_id"])} + # # data={"dependencies": context["representation"]["id"]} # ) def process_asset(self, @@ -148,8 +148,8 @@ class JsonLayoutLoader(plugin.AssetLoader): options: Additional settings dictionary """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] asset_name = plugin.prepare_scene_name(folder_name, product_name) unique_number = plugin.get_unique_number(folder_name, product_name) @@ -177,11 +177,11 @@ class JsonLayoutLoader(plugin.AssetLoader): "name": name, "namespace": namespace or '', "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "libpath": libpath, "asset_name": asset_name, - "parent": str(context["representation"]["parent"]), - "productType": context["subset"]["data"]["family"], + "parent": context["representation"]["versionId"], + "productType": context["product"]["productType"], "objectName": group_name } @@ -197,16 +197,16 @@ class JsonLayoutLoader(plugin.AssetLoader): will not be removed, only unlinked. Normally this should not be the case though. """ - repre_doc = context["representation"] + repre_entity = context["representation"] object_name = container["objectName"] asset_group = bpy.data.objects.get(object_name) - libpath = Path(get_representation_path(repre_doc)) + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert asset_group, ( @@ -270,7 +270,7 @@ class JsonLayoutLoader(plugin.AssetLoader): asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) - metadata["representation"] = str(repre_doc["_id"]) + metadata["representation"] = repre_entity["id"] def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. diff --git a/client/ayon_core/hosts/blender/plugins/load/load_look.py b/client/ayon_core/hosts/blender/plugins/load/load_look.py index 59896e0ae0..7cefbd5148 100644 --- a/client/ayon_core/hosts/blender/plugins/load/load_look.py +++ b/client/ayon_core/hosts/blender/plugins/load/load_look.py @@ -93,8 +93,8 @@ class BlendLookLoader(plugin.AssetLoader): """ libpath = self.filepath_from_context(context) - folder_name = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] lib_container = plugin.prepare_scene_name( folder_name, product_name @@ -130,8 +130,8 @@ class BlendLookLoader(plugin.AssetLoader): metadata["objects"] = objects metadata["materials"] = materials - metadata["parent"] = str(context["representation"]["parent"]) - metadata["product_type"] = context["subset"]["data"]["family"] + metadata["parent"] = context["representation"]["versionId"] + metadata["product_type"] = context["product"]["productType"] nodes = list(container.objects) nodes.append(container) @@ -140,14 +140,14 @@ class BlendLookLoader(plugin.AssetLoader): def update(self, container: Dict, context: Dict): collection = bpy.data.collections.get(container["objectName"]) - repre_doc = context["representation"] - libpath = Path(get_representation_path(repre_doc)) + repre_entity = context["representation"] + libpath = Path(get_representation_path(repre_entity)) extension = libpath.suffix.lower() self.log.info( "Container: %s\nRepresentation: %s", pformat(container, indent=2), - pformat(repre_doc, indent=2), + pformat(repre_entity, indent=2), ) assert collection, ( @@ -202,7 +202,7 @@ class BlendLookLoader(plugin.AssetLoader): collection_metadata["objects"] = objects collection_metadata["materials"] = materials collection_metadata["libpath"] = str(libpath) - collection_metadata["representation"] = str(repre_doc["_id"]) + collection_metadata["representation"] = repre_entity["id"] def remove(self, container: Dict) -> bool: collection = bpy.data.collections.get(container["objectName"]) diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_abc.py b/client/ayon_core/hosts/blender/plugins/publish/extract_abc.py index cf753637ea..094f88fd8c 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_abc.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_abc.py @@ -19,7 +19,7 @@ class ExtractABC(publish.Extractor, publish.OptionalPyblishPluginMixin): # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.abc" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_abc_animation.py b/client/ayon_core/hosts/blender/plugins/publish/extract_abc_animation.py index 0086dccd67..f33af13282 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_abc_animation.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_abc_animation.py @@ -23,7 +23,7 @@ class ExtractAnimationABC( # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.abc" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py b/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py index 731e91ad76..19fe9c6271 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_blend.py @@ -23,7 +23,7 @@ class ExtractBlend(publish.Extractor, publish.OptionalPyblishPluginMixin): # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.blend" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py b/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py index 64009c4b6c..315fbb19af 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_blend_animation.py @@ -26,7 +26,7 @@ class ExtractBlendAnimation( # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.blend" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py b/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py index ff14d70696..cc783e552c 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_camera_abc.py @@ -21,7 +21,7 @@ class ExtractCameraABC(publish.Extractor, publish.OptionalPyblishPluginMixin): # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.abc" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_camera_fbx.py b/client/ayon_core/hosts/blender/plugins/publish/extract_camera_fbx.py index 03059f1e13..bcaf9ebc44 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_camera_fbx.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_camera_fbx.py @@ -20,7 +20,7 @@ class ExtractCamera(publish.Extractor, publish.OptionalPyblishPluginMixin): # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.fbx" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py b/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py index 8fea077e7c..7ebda2c4cd 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_fbx.py @@ -21,7 +21,7 @@ class ExtractFBX(publish.Extractor, publish.OptionalPyblishPluginMixin): # Define extract output file path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" filename = f"{instance_name}.fbx" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_fbx_animation.py b/client/ayon_core/hosts/blender/plugins/publish/extract_fbx_animation.py index b98167c741..ae02909152 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -145,7 +145,7 @@ class ExtractAnimationFBX( root.select_set(True) armature.select_set(True) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" fbx_filename = f"{instance_name}_{armature.name}.fbx" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_layout.py b/client/ayon_core/hosts/blender/plugins/publish/extract_layout.py index 16c0392070..0679483dd5 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_layout.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_layout.py @@ -5,7 +5,8 @@ import bpy import bpy_extras import bpy_extras.anim_utils -from ayon_core.client import get_representation_by_name +from ayon_api import get_representations + from ayon_core.pipeline import publish from ayon_core.hosts.blender.api import plugin from ayon_core.hosts.blender.api.pipeline import AVALON_PROPERTY @@ -134,6 +135,8 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): fbx_count = 0 project_name = instance.context.data["projectName"] + version_ids = set() + filtered_assets = [] for asset in asset_group.children: metadata = asset.get(AVALON_PROPERTY) if not metadata: @@ -146,42 +149,47 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): ) continue + filtered_assets.append((asset, metadata)) + version_ids.add(metadata["parent"]) + + repre_entities = get_representations( + project_name, + representation_names={"blend", "fbx", "abc"}, + version_ids=version_ids, + fields={"id", "versionId", "name"} + ) + repre_mapping_by_version_id = { + version_id: {} + for version_id in version_ids + } + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + repre_mapping_by_version_id[version_id][repre_entity["name"]] = ( + repre_entity + ) + + for asset, metadata in filtered_assets: version_id = metadata["parent"] product_type = metadata.get("product_type") if product_type is None: product_type = metadata["family"] + repres_by_name = repre_mapping_by_version_id[version_id] + self.log.debug("Parent: {}".format(version_id)) - # Get blend reference - blend = get_representation_by_name( - project_name, "blend", version_id, fields=["_id"] - ) - blend_id = None - if blend: - blend_id = blend["_id"] - # Get fbx reference - fbx = get_representation_by_name( - project_name, "fbx", version_id, fields=["_id"] - ) - fbx_id = None - if fbx: - fbx_id = fbx["_id"] - # Get abc reference - abc = get_representation_by_name( - project_name, "abc", version_id, fields=["_id"] - ) - abc_id = None - if abc: - abc_id = abc["_id"] - - json_element = {} - if blend_id: - json_element["reference"] = str(blend_id) - if fbx_id: - json_element["reference_fbx"] = str(fbx_id) - if abc_id: - json_element["reference_abc"] = str(abc_id) - + # Get blend, fbx and abc reference + blend_id = repres_by_name.get("blend", {}).get("id") + fbx_id = repres_by_name.get("fbx", {}).get("id") + abc_id = repres_by_name.get("abc", {}).get("id") + json_element = { + key: value + for key, value in ( + ("reference", blend_id), + ("reference_fbx", fbx_id), + ("reference_abc", abc_id), + ) + if value + } json_element["product_type"] = product_type json_element["instance_name"] = asset.name json_element["asset_name"] = metadata["asset_name"] @@ -228,7 +236,7 @@ class ExtractLayout(publish.Extractor, publish.OptionalPyblishPluginMixin): json_data.append(json_element) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] instance_name = f"{folder_name}_{product_name}" json_filename = f"{instance_name}.json" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_playblast.py b/client/ayon_core/hosts/blender/plugins/publish/extract_playblast.py index acb09d0d77..ce6f40f967 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_playblast.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_playblast.py @@ -55,7 +55,7 @@ class ExtractPlayblast(publish.Extractor, publish.OptionalPyblishPluginMixin): # get output path stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] filename = f"{folder_name}_{product_name}" diff --git a/client/ayon_core/hosts/blender/plugins/publish/extract_thumbnail.py b/client/ayon_core/hosts/blender/plugins/publish/extract_thumbnail.py index 89168fb9c9..4330c57d99 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/extract_thumbnail.py +++ b/client/ayon_core/hosts/blender/plugins/publish/extract_thumbnail.py @@ -32,7 +32,7 @@ class ExtractThumbnail(publish.Extractor): return stagingdir = self.staging_dir(instance) - folder_name = instance.data["assetEntity"]["name"] + folder_name = instance.data["folderEntity"]["name"] product_name = instance.data["productName"] filename = f"{folder_name}_{product_name}" diff --git a/client/ayon_core/hosts/blender/plugins/publish/integrate_animation.py b/client/ayon_core/hosts/blender/plugins/publish/integrate_animation.py index a10144ebf5..5d3a1dac93 100644 --- a/client/ayon_core/hosts/blender/plugins/publish/integrate_animation.py +++ b/client/ayon_core/hosts/blender/plugins/publish/integrate_animation.py @@ -44,7 +44,7 @@ class IntegrateAnimation( break if not rep: continue - obj_id = rep["representation"]["_id"] + obj_id = rep["representation"]["id"] if obj_id: json_dict["representation_id"] = str(obj_id) diff --git a/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py b/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py index bf1b4937cd..73b368e4e3 100644 --- a/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py +++ b/client/ayon_core/hosts/celaction/hooks/pre_celaction_setup.py @@ -16,9 +16,9 @@ class CelactionPrelaunchHook(PreLaunchHook): launch_types = {LaunchTypes.local} def execute(self): - asset_doc = self.data["asset_doc"] - width = asset_doc["data"]["resolutionWidth"] - height = asset_doc["data"]["resolutionHeight"] + folder_attributes = self.data["folder_entity"]["attrib"] + width = folder_attributes["resolutionWidth"] + height = folder_attributes["resolutionHeight"] # Add workfile path to launch arguments workfile_path = self.workfile_path() diff --git a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_instances.py b/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_instances.py index 4306a53bfe..7c22201e3e 100644 --- a/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_instances.py +++ b/client/ayon_core/hosts/celaction/plugins/publish/collect_celaction_instances.py @@ -1,8 +1,6 @@ import os import pyblish.api -from ayon_core.client import get_asset_name_identifier - class CollectCelactionInstances(pyblish.api.ContextPlugin): """ Adds the celaction render instances """ @@ -16,24 +14,20 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) version = context.data["version"] - asset_entity = context.data["assetEntity"] - project_entity = context.data["projectEntity"] - asset_name = get_asset_name_identifier(asset_entity) + folder_entity = context.data["folderEntity"] + + folder_attributes = folder_entity["attrib"] shared_instance_data = { - "folderPath": asset_name, - "frameStart": asset_entity["data"]["frameStart"], - "frameEnd": asset_entity["data"]["frameEnd"], - "handleStart": asset_entity["data"]["handleStart"], - "handleEnd": asset_entity["data"]["handleEnd"], - "fps": asset_entity["data"]["fps"], - "resolutionWidth": asset_entity["data"].get( - "resolutionWidth", - project_entity["data"]["resolutionWidth"]), - "resolutionHeight": asset_entity["data"].get( - "resolutionHeight", - project_entity["data"]["resolutionHeight"]), + "folderPath": folder_entity["path"], + "frameStart": folder_attributes["frameStart"], + "frameEnd": folder_attributes["frameEnd"], + "handleStart": folder_attributes["handleStart"], + "handleEnd": folder_attributes["handleEnd"], + "fps": folder_attributes["fps"], + "resolutionWidth": folder_attributes["resolutionWidth"], + "resolutionHeight": folder_attributes["resolutionHeight"], "pixelAspect": 1, "step": 1, "version": version @@ -83,7 +77,7 @@ class CollectCelactionInstances(pyblish.api.ContextPlugin): # getting instance state instance.data["publish"] = True - # add assetEntity data into instance + # add folderEntity data into instance instance.data.update({ "label": "{} - farm".format(product_name), "productType": product_type, diff --git a/client/ayon_core/hosts/flame/api/pipeline.py b/client/ayon_core/hosts/flame/api/pipeline.py index 532f89b5e9..a902b9ee73 100644 --- a/client/ayon_core/hosts/flame/api/pipeline.py +++ b/client/ayon_core/hosts/flame/api/pipeline.py @@ -73,7 +73,7 @@ def containerise(flame_clip_segment, "name": str(name), "namespace": str(namespace), "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } if data: diff --git a/client/ayon_core/hosts/flame/api/plugin.py b/client/ayon_core/hosts/flame/api/plugin.py index cf28a3cef3..c5667eb75a 100644 --- a/client/ayon_core/hosts/flame/api/plugin.py +++ b/client/ayon_core/hosts/flame/api/plugin.py @@ -748,18 +748,16 @@ class ClipLoader(LoaderPlugin): Returns: str: colorspace name or None """ - version = context['version'] - version_data = version.get("data", {}) - colorspace = version_data.get( - "colorspace", None - ) + version_entity = context["version"] + version_attributes = version_entity["attrib"] + colorspace = version_attributes.get("colorSpace") if ( not colorspace or colorspace == "Unknown" ): colorspace = context["representation"]["data"].get( - "colorspace", None) + "colorspace") return colorspace diff --git a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py b/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py index 391332d368..b7fc431352 100644 --- a/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py +++ b/client/ayon_core/hosts/flame/hooks/pre_flame_setup.py @@ -36,8 +36,8 @@ class FlamePrelaunch(PreLaunchHook): self.flame_pythonpath = _env["AYON_FLAME_PYTHONPATH"] """Hook entry method.""" - project_doc = self.data["project_doc"] - project_name = project_doc["name"] + project_entity = self.data["project_entity"] + project_name = project_entity["name"] volume_name = _env.get("FLAME_WIRETAP_VOLUME") # get image io @@ -63,20 +63,22 @@ class FlamePrelaunch(PreLaunchHook): hostname = socket.gethostname() # not returning wiretap host name self.log.debug("Collected user \"{}\"".format(user_name)) - self.log.info(pformat(project_doc)) - _db_p_data = project_doc["data"] - width = _db_p_data["resolutionWidth"] - height = _db_p_data["resolutionHeight"] - fps = float(_db_p_data["fps"]) + self.log.info(pformat(project_entity)) + project_attribs = project_entity["attrib"] + width = project_attribs["resolutionWidth"] + height = project_attribs["resolutionHeight"] + fps = float(project_attribs["fps"]) project_data = { - "Name": project_doc["name"], - "Nickname": _db_p_data["code"], + "Name": project_entity["name"], + "Nickname": project_entity["code"], "Description": "Created by OpenPype", - "SetupDir": project_doc["name"], + "SetupDir": project_entity["name"], "FrameWidth": int(width), "FrameHeight": int(height), - "AspectRatio": float((width / height) * _db_p_data["pixelAspect"]), + "AspectRatio": float( + (width / height) * project_attribs["pixelAspect"] + ), "FrameRate": self._get_flame_fps(fps) } diff --git a/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py b/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py index c73ee7510c..e8eb2b9fab 100644 --- a/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py +++ b/client/ayon_core/hosts/flame/plugins/create/create_shot_clip.py @@ -207,14 +207,14 @@ class CreateShotClip(opfapi.Creator): "value": ["[ track name ]", "main", "bg", "fg", "bg", "animatic"], "type": "QComboBox", - "label": "Subset Name", + "label": "Product Name", "target": "ui", "toolTip": "chose product name pattern, if [ track name ] is selected, name of track layer will be used", # noqa "order": 0}, "productType": { "value": ["plate", "take"], "type": "QComboBox", - "label": "Subset Family", + "label": "Product Type", "target": "ui", "toolTip": "What use of this product is for", # noqa "order": 1}, "reviewTrack": { diff --git a/client/ayon_core/hosts/flame/plugins/load/load_clip.py b/client/ayon_core/hosts/flame/plugins/load/load_clip.py index 72a6f2a585..faa2b7145d 100644 --- a/client/ayon_core/hosts/flame/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/flame/plugins/load/load_clip.py @@ -48,9 +48,9 @@ class LoadClip(opfapi.ClipLoader): self.fpd = fproject.current_workspace.desktop # load clip to timeline and get main variables - version = context['version'] - version_data = version.get("data", {}) - version_name = version.get("name", None) + version_entity = context["version"] + version_attributes = version_entity["attrib"] + version_name = version_entity["version"] colorspace = self.get_colorspace(context) # in case output is not in context replace key to representation @@ -112,11 +112,10 @@ class LoadClip(opfapi.ClipLoader): ] # move all version data keys to tag data - data_imprint = {} - for key in add_keys: - data_imprint.update({ - key: version_data.get(key, str(None)) - }) + data_imprint = { + key: version_attributes.get(key, str(None)) + for key in add_keys + } # add variables related to version context data_imprint.update({ @@ -187,20 +186,20 @@ class LoadClip(opfapi.ClipLoader): # """ Updating previously loaded clips # """ # # load clip to timeline and get main variables - # repre_doc = context['representation'] + # repre_entity = context['representation'] # name = container['name'] # namespace = container['namespace'] # track_item = phiero.get_track_items( # track_item_name=namespace) # version = io.find_one({ # "type": "version", - # "_id": repre_doc["parent"] + # "id": repre_entity["versionId"] # }) # version_data = version.get("data", {}) # version_name = version.get("name", None) - # colorspace = version_data.get("colorspace", None) + # colorspace = version_data.get("colorSpace", None) # object_name = "{}_{}".format(name, namespace) - # file = get_representation_path(repre_doc).replace("\\", "/") + # file = get_representation_path(repre_entity).replace("\\", "/") # clip = track_item.source() # # reconnect media to new path @@ -225,7 +224,7 @@ class LoadClip(opfapi.ClipLoader): # # add variables related to version context # data_imprint.update({ - # "representation": str(repre_doc["_id"]), + # "representation": repre_entity["id"], # "version": version_name, # "colorspace": colorspace, # "objectName": object_name diff --git a/client/ayon_core/hosts/flame/plugins/load/load_clip_batch.py b/client/ayon_core/hosts/flame/plugins/load/load_clip_batch.py index 9f81103cb4..7eaaa308df 100644 --- a/client/ayon_core/hosts/flame/plugins/load/load_clip_batch.py +++ b/client/ayon_core/hosts/flame/plugins/load/load_clip_batch.py @@ -45,9 +45,9 @@ class LoadClipBatch(opfapi.ClipLoader): self.batch = options.get("batch") or flame.batch # load clip to timeline and get main variables - version = context['version'] - version_data = version.get("data", {}) - version_name = version.get("name", None) + version_entity = context["version"] + version_attributes =version_entity["attrib"] + version_name = version_entity["version"] colorspace = self.get_colorspace(context) clip_name_template = self.clip_name_template @@ -59,20 +59,20 @@ class LoadClipBatch(opfapi.ClipLoader): layer_rename_template = layer_rename_template.replace( "output", "representation") - asset_doc = context["asset"] - subset_doc = context["subset"] + folder_entity = context["folder"] + product_entity = context["product"] formatting_data = deepcopy(context["representation"]["context"]) formatting_data["batch"] = self.batch.name.get_value() formatting_data.update({ - "asset": asset_doc["name"], + "asset": folder_entity["name"], "folder": { - "name": asset_doc["name"], + "name": folder_entity["name"], }, - "subset": subset_doc["name"], - "family": subset_doc["data"]["family"], + "subset": product_entity["name"], + "family": product_entity["productType"], "product": { - "name": subset_doc["name"], - "type": subset_doc["data"]["family"], + "name": product_entity["name"], + "type": product_entity["productType"], } }) @@ -129,7 +129,7 @@ class LoadClipBatch(opfapi.ClipLoader): # move all version data keys to tag data data_imprint = { - key: version_data.get(key, str(None)) + key: version_attributes.get(key, str(None)) for key in add_keys } # add variables related to version context diff --git a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_otio.py b/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_otio.py index 6d04d53cea..7609ea7879 100644 --- a/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_otio.py +++ b/client/ayon_core/hosts/flame/plugins/publish/collect_timeline_otio.py @@ -1,6 +1,5 @@ import pyblish.api -from ayon_core.client import get_asset_name_identifier import ayon_core.hosts.flame.api as opfapi from ayon_core.hosts.flame.otio import flame_export from ayon_core.pipeline.create import get_product_name @@ -18,31 +17,33 @@ class CollecTimelineOTIO(pyblish.api.ContextPlugin): variant = "otioTimeline" # main - asset_doc = context.data["assetEntity"] - task_name = context.data["task"] + folder_entity = context.data["folderEntity"] project = opfapi.get_current_project() sequence = opfapi.get_current_sequence(opfapi.CTX.selection) # create product name + task_entity = context.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] product_name = get_product_name( context.data["projectName"], - asset_doc, task_name, + task_type, context.data["hostName"], product_type, variant, project_settings=context.data["project_settings"] ) - folder_path = get_asset_name_identifier(asset_doc) - # adding otio timeline to context with opfapi.maintained_segment_selection(sequence) as selected_seg: otio_timeline = flame_export.create_otio_timeline(sequence) instance_data = { "name": product_name, - "folderPath": folder_path, + "folderPath": folder_entity["path"], "productName": product_name, "productType": product_type, "family": product_type, diff --git a/client/ayon_core/hosts/flame/plugins/publish/integrate_batch_group.py b/client/ayon_core/hosts/flame/plugins/publish/integrate_batch_group.py index a66ee9f2c0..d8669f836d 100644 --- a/client/ayon_core/hosts/flame/plugins/publish/integrate_batch_group.py +++ b/client/ayon_core/hosts/flame/plugins/publish/integrate_batch_group.py @@ -219,7 +219,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): # update task data in anatomy data project_task_types = anatomy_obj["tasks"] - task_code = project_task_types.get(task_type, {}).get("short_name") + task_code = project_task_types.get(task_type, {}).get("shortName") anatomy_data.update({ "task": { "name": task_name, @@ -247,7 +247,7 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): os.makedirs(render_dir_path, mode=0o777) # TODO: add most of these to `imageio/flame/batch/write_node` - name = "{project[code]}_{asset}_{task[name]}".format( + name = "{project[code]}_{folder[name]}_{task[name]}".format( **anatomy_data ) @@ -321,16 +321,17 @@ class IntegrateBatchGroup(pyblish.api.InstancePlugin): )) def _get_shot_task_dir_path(self, instance, task_data): - project_doc = instance.data["projectEntity"] - asset_entity = instance.data["assetEntity"] + project_entity = instance.data["projectEntity"] + folder_entity = instance.data["folderEntity"] + task_entity = instance.data["taskEntity"] anatomy = instance.context.data["anatomy"] project_settings = instance.context.data["project_settings"] return get_workdir( - project_doc, - asset_entity, - task_data["name"], + project_entity, + folder_entity, + task_entity, "flame", - anatomy, + anatomy=anatomy, project_settings=project_settings ) diff --git a/client/ayon_core/hosts/fusion/api/__init__.py b/client/ayon_core/hosts/fusion/api/__init__.py index aabc624016..ddd718e606 100644 --- a/client/ayon_core/hosts/fusion/api/__init__.py +++ b/client/ayon_core/hosts/fusion/api/__init__.py @@ -9,7 +9,7 @@ from .pipeline import ( from .lib import ( maintained_selection, update_frame_range, - set_asset_framerange, + set_current_context_framerange, get_current_comp, get_bmd_library, comp_lock_and_undo_chunk @@ -29,7 +29,7 @@ __all__ = [ # lib "maintained_selection", "update_frame_range", - "set_asset_framerange", + "set_current_context_framerange", "get_current_comp", "get_bmd_library", "comp_lock_and_undo_chunk", diff --git a/client/ayon_core/hosts/fusion/api/lib.py b/client/ayon_core/hosts/fusion/api/lib.py index b31f812c1b..e5bf4b5a44 100644 --- a/client/ayon_core/hosts/fusion/api/lib.py +++ b/client/ayon_core/hosts/fusion/api/lib.py @@ -4,19 +4,8 @@ import re import contextlib from ayon_core.lib import Logger -from ayon_core.client import ( - get_asset_by_name, - get_subset_by_name, - get_last_version_by_subset_id, - get_representation_by_id, - get_representation_by_name, - get_representation_parents, -) -from ayon_core.pipeline import ( - switch_container, - get_current_project_name, -) -from ayon_core.pipeline.context_tools import get_current_project_asset + +from ayon_core.pipeline.context_tools import get_current_project_folder self = sys.modules[__name__] self._project = None @@ -63,23 +52,25 @@ def update_frame_range(start, end, comp=None, set_render_range=True, comp.SetAttrs(attrs) -def set_asset_framerange(): - """Set Comp's frame range based on current asset""" - asset_doc = get_current_project_asset() - start = asset_doc["data"]["frameStart"] - end = asset_doc["data"]["frameEnd"] - handle_start = asset_doc["data"]["handleStart"] - handle_end = asset_doc["data"]["handleEnd"] +def set_current_context_framerange(): + """Set Comp's frame range based on current folder.""" + folder_entity = get_current_project_folder() + folder_attributes = folder_entity["attrib"] + start = folder_attributes["frameStart"] + end = folder_attributes["frameEnd"] + handle_start = folder_attributes["handleStart"] + handle_end = folder_attributes["handleEnd"] update_frame_range(start, end, set_render_range=True, handle_start=handle_start, handle_end=handle_end) -def set_asset_resolution(): - """Set Comp's resolution width x height default based on current asset""" - asset_doc = get_current_project_asset() - width = asset_doc["data"]["resolutionWidth"] - height = asset_doc["data"]["resolutionHeight"] +def set_current_context_resolution(): + """Set Comp's resolution width x height default based on current folder""" + folder_entity = get_current_project_folder() + folder_attributes = folder_entity["attrib"] + width = folder_attributes["resolutionWidth"] + height = folder_attributes["resolutionHeight"] comp = get_current_comp() print("Setting comp frame format resolution to {}x{}".format(width, @@ -91,7 +82,7 @@ def set_asset_resolution(): def validate_comp_prefs(comp=None, force_repair=False): - """Validate current comp defaults with asset settings. + """Validate current comp defaults with folder settings. Validates fps, resolutionWidth, resolutionHeight, aspectRatio. @@ -103,22 +94,23 @@ def validate_comp_prefs(comp=None, force_repair=False): log = Logger.get_logger("validate_comp_prefs") - fields = [ - "name", - "data.fps", - "data.resolutionWidth", - "data.resolutionHeight", - "data.pixelAspect" - ] - asset_doc = get_current_project_asset(fields=fields) - asset_data = asset_doc["data"] + fields = { + "path", + "attrib.fps", + "attrib.resolutionWidth", + "attrib.resolutionHeight", + "attrib.pixelAspect", + } + folder_entity = get_current_project_folder(fields=fields) + folder_path = folder_entity["path"] + folder_attributes = folder_entity["attrib"] comp_frame_format_prefs = comp.GetPrefs("Comp.FrameFormat") # Pixel aspect ratio in Fusion is set as AspectX and AspectY so we convert # the data to something that is more sensible to Fusion - asset_data["pixelAspectX"] = asset_data.pop("pixelAspect") - asset_data["pixelAspectY"] = 1.0 + folder_attributes["pixelAspectX"] = folder_attributes.pop("pixelAspect") + folder_attributes["pixelAspectY"] = 1.0 validations = [ ("fps", "Rate", "FPS"), @@ -130,23 +122,23 @@ def validate_comp_prefs(comp=None, force_repair=False): invalid = [] for key, comp_key, label in validations: - asset_value = asset_data[key] + folder_value = folder_attributes[key] comp_value = comp_frame_format_prefs.get(comp_key) - if asset_value != comp_value: + if folder_value != comp_value: invalid_msg = "{} {} should be {}".format(label, comp_value, - asset_value) + folder_value) invalid.append(invalid_msg) if not force_repair: # Do not log warning if we force repair anyway log.warning( - "Comp {pref} {value} does not match asset " - "'{asset_name}' {pref} {asset_value}".format( + "Comp {pref} {value} does not match folder " + "'{folder_path}' {pref} {folder_value}".format( pref=label, value=comp_value, - asset_name=asset_doc["name"], - asset_value=asset_value) + folder_path=folder_path, + folder_value=folder_value) ) if invalid: @@ -154,7 +146,7 @@ def validate_comp_prefs(comp=None, force_repair=False): def _on_repair(): attributes = dict() for key, comp_key, _label in validations: - value = asset_data[key] + value = folder_value[key] comp_key_full = "Comp.FrameFormat.{}".format(comp_key) attributes[comp_key_full] = value comp.SetPrefs(attributes) @@ -170,7 +162,7 @@ def validate_comp_prefs(comp=None, force_repair=False): dialog = SimplePopup(parent=menu.menu) dialog.setWindowTitle("Fusion comp has invalid configuration") - msg = "Comp preferences mismatches '{}'".format(asset_doc["name"]) + msg = "Comp preferences mismatches '{}'".format(folder_path) msg += "\n" + "\n".join(invalid) dialog.set_message(msg) dialog.set_button_text("Repair") diff --git a/client/ayon_core/hosts/fusion/api/menu.py b/client/ayon_core/hosts/fusion/api/menu.py index a2b0a7b628..642287eb10 100644 --- a/client/ayon_core/hosts/fusion/api/menu.py +++ b/client/ayon_core/hosts/fusion/api/menu.py @@ -10,10 +10,10 @@ from ayon_core.hosts.fusion.scripts import ( duplicate_with_inputs, ) from ayon_core.hosts.fusion.api.lib import ( - set_asset_framerange, - set_asset_resolution, + set_current_context_framerange, + set_current_context_resolution, ) -from ayon_core.pipeline import get_current_asset_name +from ayon_core.pipeline import get_current_folder_path from ayon_core.resources import get_ayon_icon_filepath from ayon_core.tools.utils import get_qt_app @@ -49,15 +49,15 @@ class OpenPypeMenu(QtWidgets.QWidget): self.render_mode_widget = None self.setWindowTitle(MENU_LABEL) - asset_label = QtWidgets.QLabel("Context", self) - asset_label.setStyleSheet( + context_label = QtWidgets.QLabel("Context", self) + context_label.setStyleSheet( """QLabel { font-size: 14px; font-weight: 600; color: #5f9fb8; }""" ) - asset_label.setAlignment(QtCore.Qt.AlignHCenter) + context_label.setAlignment(QtCore.Qt.AlignHCenter) workfiles_btn = QtWidgets.QPushButton("Workfiles...", self) create_btn = QtWidgets.QPushButton("Create...", self) @@ -74,7 +74,7 @@ class OpenPypeMenu(QtWidgets.QWidget): layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(10, 20, 10, 20) - layout.addWidget(asset_label) + layout.addWidget(context_label) layout.addSpacing(20) @@ -103,7 +103,7 @@ class OpenPypeMenu(QtWidgets.QWidget): self.setLayout(layout) # Store reference so we can update the label - self.asset_label = asset_label + self.context_label = context_label workfiles_btn.clicked.connect(self.on_workfile_clicked) create_btn.clicked.connect(self.on_create_clicked) @@ -131,8 +131,8 @@ class OpenPypeMenu(QtWidgets.QWidget): def on_task_changed(self): # Update current context label - label = get_current_asset_name() - self.asset_label.setText(label) + label = get_current_folder_path() + self.context_label.setText(label) def register_callback(self, name, fn): # Create a wrapper callback that we only store @@ -168,10 +168,10 @@ class OpenPypeMenu(QtWidgets.QWidget): duplicate_with_inputs.duplicate_with_input_connections() def on_set_resolution_clicked(self): - set_asset_resolution() + set_current_context_resolution() def on_set_framerange_clicked(self): - set_asset_framerange() + set_current_context_framerange() def launch_openpype_menu(): diff --git a/client/ayon_core/hosts/fusion/api/pipeline.py b/client/ayon_core/hosts/fusion/api/pipeline.py index 0e9e0724c7..3bb66619a9 100644 --- a/client/ayon_core/hosts/fusion/api/pipeline.py +++ b/client/ayon_core/hosts/fusion/api/pipeline.py @@ -252,7 +252,7 @@ def imprint_container(tool, ("name", str(name)), ("namespace", str(namespace)), ("loader", str(loader)), - ("representation", str(context["representation"]["_id"])), + ("representation", context["representation"]["id"]), ] for key, value in data: diff --git a/client/ayon_core/hosts/fusion/api/plugin.py b/client/ayon_core/hosts/fusion/api/plugin.py index 95db8126e7..f63b5eaec3 100644 --- a/client/ayon_core/hosts/fusion/api/plugin.py +++ b/client/ayon_core/hosts/fusion/api/plugin.py @@ -138,7 +138,7 @@ class GenericCreateSaver(Creator): # get output format ext = data["creator_attributes"]["image_format"] - # Subset change detected + # Product change detected product_type = formatting_data["productType"] f_product_name = formatting_data["productName"] diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_saver.py b/client/ayon_core/hosts/fusion/plugins/create/create_saver.py index b5abb2d949..b6cda1f302 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_saver.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_saver.py @@ -15,7 +15,7 @@ class CreateSaver(GenericCreateSaver): product_type = "render" description = "Fusion Saver to generate image sequence" - default_frame_range_option = "asset_db" + default_frame_range_option = "current_folder" def get_detail_description(self): return """Fusion Saver to generate image sequence. @@ -24,7 +24,7 @@ class CreateSaver(GenericCreateSaver): product type. (But can publish even single frame 'render'.) Select what should be source of render range: - - "Current asset context" - values set on Asset in DB (Ftrack) + - "Current Folder context" - values set on folder on AYON server - "From render in/out" - from node itself - "From composition timeline" - from timeline @@ -50,7 +50,7 @@ class CreateSaver(GenericCreateSaver): def _get_frame_range_enum(self): frame_range_options = { - "asset_db": "Current asset context", + "current_folder": "Current Folder context", "render_range": "From render in/out", "comp_range": "From composition timeline", } diff --git a/client/ayon_core/hosts/fusion/plugins/create/create_workfile.py b/client/ayon_core/hosts/fusion/plugins/create/create_workfile.py index dfd9da3df1..a2fe027ef4 100644 --- a/client/ayon_core/hosts/fusion/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/fusion/plugins/create/create_workfile.py @@ -1,7 +1,8 @@ +import ayon_api + from ayon_core.hosts.fusion.api import ( get_current_comp ) -from ayon_core.client import get_asset_by_name from ayon_core.pipeline import ( AutoCreator, CreatedInstance, @@ -54,7 +55,6 @@ class FusionWorkfileCreator(AutoCreator): comp.SetData(self.data_key, data) def create(self, options=None): - comp = get_current_comp() if not comp: self.log.error("Unable to find current comp") @@ -67,33 +67,37 @@ class FusionWorkfileCreator(AutoCreator): break project_name = self.create_context.get_current_project_name() - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - if existing_instance is None: - existing_instance_asset = None - else: - existing_instance_asset = existing_instance["folderPath"] + existing_folder_path = None + if existing_instance is not None: + existing_folder_path = existing_instance["folderPath"] if existing_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant, } data.update(self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, None @@ -107,17 +111,22 @@ class FusionWorkfileCreator(AutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_instance_asset != asset_name + existing_folder_path != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/fusion/plugins/load/actions.py b/client/ayon_core/hosts/fusion/plugins/load/actions.py index f67878bcff..2fac884b2e 100644 --- a/client/ayon_core/hosts/fusion/plugins/load/actions.py +++ b/client/ayon_core/hosts/fusion/plugins/load/actions.py @@ -27,11 +27,10 @@ class FusionSetFrameRangeLoader(load.LoaderPlugin): from ayon_core.hosts.fusion.api import lib - version = context['version'] - version_data = version.get("data", {}) + version_attributes = context["version"]["attrib"] - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + start = version_attributes.get("frameStart", None) + end = version_attributes.get("frameEnd", None) if start is None or end is None: print("Skipping setting frame range because start or " @@ -62,11 +61,9 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin): from ayon_core.hosts.fusion.api import lib - version = context['version'] - version_data = version.get("data", {}) - - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + version_attributes = context["version"]["attrib"] + start = version_attributes.get("frameStart", None) + end = version_attributes.get("frameEnd", None) if start is None or end is None: print("Skipping setting frame range because start or " @@ -74,7 +71,7 @@ class FusionSetFrameRangeWithHandlesLoader(load.LoaderPlugin): return # Include handles - start -= version_data.get("handleStart", 0) - end += version_data.get("handleEnd", 0) + start -= version_attributes.get("handleStart", 0) + end += version_attributes.get("handleEnd", 0) lib.update_frame_range(start, end) diff --git a/client/ayon_core/hosts/fusion/plugins/load/load_alembic.py b/client/ayon_core/hosts/fusion/plugins/load/load_alembic.py index 17f043bb34..d78a8b0ba0 100644 --- a/client/ayon_core/hosts/fusion/plugins/load/load_alembic.py +++ b/client/ayon_core/hosts/fusion/plugins/load/load_alembic.py @@ -24,9 +24,9 @@ class FusionLoadAlembicMesh(load.LoaderPlugin): tool_type = "SurfaceAlembicMesh" def load(self, context, name, namespace, data): - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["folder"]["name"] # Create the Loader with the filename path set comp = get_current_comp() @@ -54,14 +54,14 @@ class FusionLoadAlembicMesh(load.LoaderPlugin): assert tool.ID == self.tool_type, f"Must be {self.tool_type}" comp = tool.Comp() - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) with comp_lock_and_undo_chunk(comp, "Update tool"): tool["Filename"] = path # Update the imprinted representation - tool.SetData("avalon.representation", str(repre_doc["_id"])) + tool.SetData("avalon.representation", repre_entity["id"]) def remove(self, container): tool = container["_tool"] diff --git a/client/ayon_core/hosts/fusion/plugins/load/load_fbx.py b/client/ayon_core/hosts/fusion/plugins/load/load_fbx.py index 75320431a8..d2e1885b0c 100644 --- a/client/ayon_core/hosts/fusion/plugins/load/load_fbx.py +++ b/client/ayon_core/hosts/fusion/plugins/load/load_fbx.py @@ -38,9 +38,9 @@ class FusionLoadFBXMesh(load.LoaderPlugin): tool_type = "SurfaceFBXMesh" def load(self, context, name, namespace, data): - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context["asset"]["name"] + namespace = context["folder"]["name"] # Create the Loader with the filename path set comp = get_current_comp() @@ -69,14 +69,14 @@ class FusionLoadFBXMesh(load.LoaderPlugin): assert tool.ID == self.tool_type, f"Must be {self.tool_type}" comp = tool.Comp() - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) with comp_lock_and_undo_chunk(comp, "Update tool"): tool["ImportFile"] = path # Update the imprinted representation - tool.SetData("avalon.representation", str(repre_doc["_id"])) + tool.SetData("avalon.representation", repre_entity["id"]) def remove(self, container): tool = container["_tool"] diff --git a/client/ayon_core/hosts/fusion/plugins/load/load_sequence.py b/client/ayon_core/hosts/fusion/plugins/load/load_sequence.py index 678da54ad6..dfd7e4231b 100644 --- a/client/ayon_core/hosts/fusion/plugins/load/load_sequence.py +++ b/client/ayon_core/hosts/fusion/plugins/load/load_sequence.py @@ -1,7 +1,6 @@ import contextlib import ayon_core.pipeline.load as load -from ayon_core.pipeline.load import get_representation_context from ayon_core.hosts.fusion.api import ( imprint_container, get_current_comp, @@ -149,9 +148,9 @@ class FusionLoadSequence(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context["asset"]["name"] + namespace = context["folder"]["name"] # Use the first file for now path = self.filepath_from_context(context) @@ -224,8 +223,7 @@ class FusionLoadSequence(load.LoaderPlugin): assert tool.ID == "Loader", "Must be Loader" comp = tool.Comp() - repre_doc = context["representation"] - context = get_representation_context(repre_doc) + repre_entity = context["representation"] path = self.filepath_from_context(context) # Get start frame from version data @@ -256,7 +254,7 @@ class FusionLoadSequence(load.LoaderPlugin): ) # Update the imprinted representation - tool.SetData("avalon.representation", str(repre_doc["_id"])) + tool.SetData("avalon.representation", repre_entity["id"]) def remove(self, container): tool = container["_tool"] @@ -266,17 +264,17 @@ class FusionLoadSequence(load.LoaderPlugin): with comp_lock_and_undo_chunk(comp, "Remove Loader"): tool.Delete() - def _get_start(self, version_doc, tool): + def _get_start(self, version_entity, tool): """Return real start frame of published files (incl. handles)""" - data = version_doc["data"] + attributes = version_entity["attrib"] # Get start frame directly with handle if it's in data - start = data.get("frameStartHandle") + start = attributes.get("frameStartHandle") if start is not None: return start # Get frame start without handles - start = data.get("frameStart") + start = attributes.get("frameStart") if start is None: self.log.warning( "Missing start frame for version " @@ -286,7 +284,7 @@ class FusionLoadSequence(load.LoaderPlugin): return 0 # Use `handleStart` if the data is available - handle_start = data.get("handleStart") + handle_start = attributes.get("handleStart") if handle_start: start -= handle_start diff --git a/client/ayon_core/hosts/fusion/plugins/load/load_usd.py b/client/ayon_core/hosts/fusion/plugins/load/load_usd.py index e315c84713..a609af6197 100644 --- a/client/ayon_core/hosts/fusion/plugins/load/load_usd.py +++ b/client/ayon_core/hosts/fusion/plugins/load/load_usd.py @@ -40,9 +40,9 @@ class FusionLoadUSD(load.LoaderPlugin): cls.enabled = is_usd_supported def load(self, context, name, namespace, data): - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["folder"]["name"] # Create the Loader with the filename path set comp = get_current_comp() @@ -69,14 +69,14 @@ class FusionLoadUSD(load.LoaderPlugin): assert tool.ID == self.tool_type, f"Must be {self.tool_type}" comp = tool.Comp() - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) with comp_lock_and_undo_chunk(comp, "Update tool"): tool["Filename"] = path # Update the imprinted representation - tool.SetData("avalon.representation", str(repre_doc["_id"])) + tool.SetData("avalon.representation", repre_entity["id"]) def remove(self, container): tool = container["_tool"] diff --git a/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py b/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py index 2cbd4d82f4..51d7e68fb6 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/collect_instances.py @@ -25,8 +25,8 @@ class CollectInstanceData(pyblish.api.InstancePlugin): frame_range_source = creator_attributes.get("frame_range_source") instance.data["frame_range_source"] = frame_range_source - # get asset frame ranges to all instances - # render product type instances `asset_db` render target + # get folder frame ranges to all instances + # render product type instances `current_folder` render target start = context.data["frameStart"] end = context.data["frameEnd"] handle_start = context.data["handleStart"] diff --git a/client/ayon_core/hosts/fusion/plugins/publish/extract_render_local.py b/client/ayon_core/hosts/fusion/plugins/publish/extract_render_local.py index 23a8cdb8a0..39fa20cfc0 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/extract_render_local.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/extract_render_local.py @@ -70,10 +70,10 @@ class FusionRenderLocal( # Log render status self.log.info( - "Rendered '{nm}' for asset '{ast}' under the task '{tsk}'".format( - nm=instance.data["name"], - ast=instance.data["folderPath"], - tsk=instance.data["task"], + "Rendered '{}' for folder '{}' under the task '{}'".format( + instance.data["name"], + instance.data["folderPath"], + instance.data["task"], ) ) diff --git a/client/ayon_core/hosts/fusion/plugins/publish/validate_saver_resolution.py b/client/ayon_core/hosts/fusion/plugins/publish/validate_saver_resolution.py index af8d4f41fa..17992b123c 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/validate_saver_resolution.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/validate_saver_resolution.py @@ -11,10 +11,10 @@ from ayon_core.hosts.fusion.api import comp_lock_and_undo_chunk class ValidateSaverResolution( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): - """Validate that the saver input resolution matches the asset resolution""" + """Validate that the saver input resolution matches the folder resolution""" order = pyblish.api.ValidatorOrder - label = "Validate Asset Resolution" + label = "Validate Folder Resolution" families = ["render", "image"] hosts = ["fusion"] optional = True @@ -29,7 +29,7 @@ class ValidateSaverResolution( if resolution != expected_resolution: raise PublishValidationError( "The input's resolution does not match " - "the asset's resolution {}x{}.\n\n" + "the folder's resolution {}x{}.\n\n" "The input's resolution is {}x{}.".format( expected_resolution[0], expected_resolution[1], resolution[0], resolution[1] @@ -55,8 +55,8 @@ class ValidateSaverResolution( @classmethod def get_expected_resolution(cls, instance): - data = instance.data["assetEntity"]["data"] - return data["resolutionWidth"], data["resolutionHeight"] + attributes = instance.data["folderEntity"]["attrib"] + return attributes["resolutionWidth"], attributes["resolutionHeight"] @classmethod def get_tool_resolution(cls, tool, frame): diff --git a/client/ayon_core/hosts/fusion/plugins/publish/validate_unique_subsets.py b/client/ayon_core/hosts/fusion/plugins/publish/validate_unique_subsets.py index 939ddbd117..bcd9abd8b0 100644 --- a/client/ayon_core/hosts/fusion/plugins/publish/validate_unique_subsets.py +++ b/client/ayon_core/hosts/fusion/plugins/publish/validate_unique_subsets.py @@ -10,7 +10,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): """Ensure all instances have a unique product name""" order = pyblish.api.ValidatorOrder - label = "Validate Unique Subsets" + label = "Validate Unique Products" families = ["render", "image"] hosts = ["fusion"] actions = [SelectInvalidAction] @@ -27,7 +27,7 @@ class ValidateUniqueSubsets(pyblish.api.ContextPlugin): instance ) - # Find which asset + subset combination has more than one instance + # Find which folder + subset combination has more than one instance # Those are considered invalid because they'd integrate to the same # destination. invalid = [] diff --git a/client/ayon_core/hosts/harmony/api/README.md b/client/ayon_core/hosts/harmony/api/README.md index 151b2bce9e..b9c21e1882 100644 --- a/client/ayon_core/hosts/harmony/api/README.md +++ b/client/ayon_core/hosts/harmony/api/README.md @@ -597,7 +597,7 @@ class ImageSequenceLoader(load.LoaderPlugin): read_node = harmony.send( { "function": copy_files + import_files, - "args": ["Top", files, context["version"]["data"]["subset"], 1] + "args": ["Top", files, context["product"]["name"], 1] } )["result"] @@ -614,9 +614,9 @@ class ImageSequenceLoader(load.LoaderPlugin): def update(self, container, context): node = container.pop("node") - repre_doc = context["representation"] + repre_entity = context["representation"] project_name = get_current_project_name() - version = get_version_by_id(project_name, repre_doc["parent"]) + version = get_version_by_id(project_name, repre_entity["versionId"]) files = [] for f in version["data"]["files"]: files.append( @@ -633,7 +633,7 @@ class ImageSequenceLoader(load.LoaderPlugin): ) harmony.imprint( - node, {"representation": str(repre_doc["_id"])} + node, {"representation": repre_entity["id"]} ) def remove(self, container): diff --git a/client/ayon_core/hosts/harmony/api/__init__.py b/client/ayon_core/hosts/harmony/api/__init__.py index d890215f63..bdcc43ea2f 100644 --- a/client/ayon_core/hosts/harmony/api/__init__.py +++ b/client/ayon_core/hosts/harmony/api/__init__.py @@ -11,7 +11,7 @@ from .pipeline import ( select_instance, containerise, set_scene_settings, - get_asset_settings, + get_current_context_settings, ensure_scene_settings, check_inventory, application_launch, @@ -55,7 +55,7 @@ __all__ = [ "select_instance", "containerise", "set_scene_settings", - "get_asset_settings", + "get_current_context_settings", "ensure_scene_settings", "check_inventory", "application_launch", diff --git a/client/ayon_core/hosts/harmony/api/pipeline.py b/client/ayon_core/hosts/harmony/api/pipeline.py index 863053dddc..a753a32ebb 100644 --- a/client/ayon_core/hosts/harmony/api/pipeline.py +++ b/client/ayon_core/hosts/harmony/api/pipeline.py @@ -13,7 +13,7 @@ from ayon_core.pipeline import ( AVALON_CONTAINER_ID, ) from ayon_core.pipeline.load import get_outdated_containers -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.hosts.harmony import HARMONY_HOST_DIR import ayon_core.hosts.harmony.api as harmony @@ -42,24 +42,25 @@ def set_scene_settings(settings): {"function": "PypeHarmony.setSceneSettings", "args": settings}) -def get_asset_settings(): - """Get settings on current asset from database. +def get_current_context_settings(): + """Get settings on current folder from server. Returns: dict: Scene data. """ - asset_doc = get_current_project_asset() - asset_data = asset_doc["data"] - fps = asset_data.get("fps") - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - handle_start = asset_data.get("handleStart") - handle_end = asset_data.get("handleEnd") - resolution_width = asset_data.get("resolutionWidth") - resolution_height = asset_data.get("resolutionHeight") - entity_type = asset_data.get("entityType") + folder_entity = get_current_project_folder() + folder_attributes = folder_entity["attrib"] + + fps = folder_attributes.get("fps") + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") + handle_start = folder_attributes.get("handleStart") + handle_end = folder_attributes.get("handleEnd") + resolution_width = folder_attributes.get("resolutionWidth") + resolution_height = folder_attributes.get("resolutionHeight") + entity_type = folder_attributes.get("entityType") scene_data = { "fps": fps, @@ -77,7 +78,7 @@ def get_asset_settings(): def ensure_scene_settings(): """Validate if Harmony scene has valid settings.""" - settings = get_asset_settings() + settings = get_current_context_settings() invalid_settings = [] valid_settings = {} @@ -336,7 +337,7 @@ def containerise(name, "name": name, "namespace": namespace, "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "nodes": nodes } diff --git a/client/ayon_core/hosts/harmony/js/PypeHarmony.js b/client/ayon_core/hosts/harmony/js/PypeHarmony.js index 41c8dc56ce..cf6a5e3763 100644 --- a/client/ayon_core/hosts/harmony/js/PypeHarmony.js +++ b/client/ayon_core/hosts/harmony/js/PypeHarmony.js @@ -34,7 +34,7 @@ PypeHarmony.message = function(message) { /** - * Set scene setting based on shot/asset settngs. + * Set scene setting based on folder settngs. * @function * @param {obj} settings Scene settings. */ diff --git a/client/ayon_core/hosts/harmony/js/loaders/ImageSequenceLoader.js b/client/ayon_core/hosts/harmony/js/loaders/ImageSequenceLoader.js index ebbd7163f9..a2ca5f9a99 100644 --- a/client/ayon_core/hosts/harmony/js/loaders/ImageSequenceLoader.js +++ b/client/ayon_core/hosts/harmony/js/loaders/ImageSequenceLoader.js @@ -87,7 +87,7 @@ ImageSequenceLoader.getUniqueColumnName = function(columnPrefix) { * // Arguments are in following order: * var args = [ * files, // Files in file sequences. - * asset, // Asset name. + * folderName, // Folder name. * productName, // Product name. * startFrame, // Sequence starting frame. * groupId // Unique group ID (uuid4). @@ -105,7 +105,7 @@ ImageSequenceLoader.prototype.importFiles = function(args) { var doc = $.scn; var files = args[0]; - var asset = args[1]; + var folderName = args[1]; var productName = args[2]; var startFrame = args[3]; var groupId = args[4]; @@ -124,7 +124,7 @@ ImageSequenceLoader.prototype.importFiles = function(args) { var num = 0; var name = ''; do { - name = asset + '_' + (num++) + '_' + productName; + name = folderName + '_' + (num++) + '_' + productName; } while (currentGroup.getNodeByName(name) != null); extension = filename.substr(pos+1).toLowerCase(); diff --git a/client/ayon_core/hosts/harmony/js/loaders/TemplateLoader.js b/client/ayon_core/hosts/harmony/js/loaders/TemplateLoader.js index 78167fcb39..c29e12c43b 100644 --- a/client/ayon_core/hosts/harmony/js/loaders/TemplateLoader.js +++ b/client/ayon_core/hosts/harmony/js/loaders/TemplateLoader.js @@ -30,7 +30,7 @@ var TemplateLoader = function() {}; * // arguments are in following order: * var args = [ * templatePath, // Path to tpl file. - * assetName, // Asset name. + * folderName, // Folder name. * productName, // Product name. * groupId // unique ID (uuid4) * ]; @@ -38,7 +38,7 @@ var TemplateLoader = function() {}; TemplateLoader.prototype.loadContainer = function(args) { var doc = $.scn; var templatePath = args[0]; - var assetName = args[1]; + var folderName = args[1]; var productName = args[2]; var groupId = args[3]; @@ -62,7 +62,7 @@ TemplateLoader.prototype.loadContainer = function(args) { var num = 0; var containerGroupName = ''; do { - containerGroupName = assetName + '_' + (num++) + '_' + productName; + containerGroupName = folderName + '_' + (num++) + '_' + productName; } while (currentGroup.getNodeByName(containerGroupName) != null); // import the template diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_audio.py b/client/ayon_core/hosts/harmony/plugins/load/load_audio.py index b73c82197a..0f8aab6d57 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_audio.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_audio.py @@ -42,10 +42,10 @@ class ImportAudioLoader(load.LoaderPlugin): def load(self, context, name=None, namespace=None, data=None): wav_file = get_representation_path(context["representation"]) harmony.send( - {"function": func, "args": [context["subset"]["name"], wav_file]} + {"function": func, "args": [context["product"]["name"], wav_file]} ) - product_name = context["subset"]["name"] + product_name = context["product"]["name"] return harmony.containerise( product_name, diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_background.py b/client/ayon_core/hosts/harmony/plugins/load/load_background.py index bf454a9ec7..72b26c826a 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_background.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_background.py @@ -254,7 +254,7 @@ class BackgroundLoader(load.LoaderPlugin): bg_folder = os.path.dirname(path) - product_name = context["subset"]["name"] + product_name = context["product"]["name"] # read_node_name += "_{}".format(uuid.uuid4()) container_nodes = [] @@ -281,8 +281,8 @@ class BackgroundLoader(load.LoaderPlugin): ) def update(self, container, context): - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) with open(path) as json_file: data = json.load(json_file) @@ -302,7 +302,7 @@ class BackgroundLoader(load.LoaderPlugin): print(container) - is_latest = is_representation_from_latest(repre_doc) + is_latest = is_representation_from_latest(repre_entity) for layer in sorted(layers): file_to_import = [ os.path.join(bg_folder, layer).replace("\\", "/") @@ -354,7 +354,7 @@ class BackgroundLoader(load.LoaderPlugin): harmony.imprint( container['name'], { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "nodes": container["nodes"] } ) diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_imagesequence.py b/client/ayon_core/hosts/harmony/plugins/load/load_imagesequence.py index 60b90fe42d..0fbcd03c92 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_imagesequence.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_imagesequence.py @@ -46,8 +46,8 @@ class ImageSequenceLoader(load.LoaderPlugin): else: files.append(fname.parent.joinpath(remainder[0]).as_posix()) - asset = context["asset"]["name"] - product_name = context["subset"]["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] group_id = str(uuid.uuid4()) read_node = harmony.send( @@ -55,7 +55,7 @@ class ImageSequenceLoader(load.LoaderPlugin): "function": f"PypeHarmony.Loaders.{self_name}.importFiles", # noqa: E501 "args": [ files, - asset, + folder_name, product_name, 1, group_id @@ -64,7 +64,7 @@ class ImageSequenceLoader(load.LoaderPlugin): )["result"] return harmony.containerise( - f"{asset}_{product_name}", + f"{folder_name}_{product_name}", namespace, read_node, context, @@ -83,8 +83,8 @@ class ImageSequenceLoader(load.LoaderPlugin): self_name = self.__class__.__name__ node = container.get("nodes").pop() - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) collections, remainder = clique.assemble( os.listdir(os.path.dirname(path)) ) @@ -111,7 +111,7 @@ class ImageSequenceLoader(load.LoaderPlugin): ) # Colour node. - if is_representation_from_latest(repre_doc): + if is_representation_from_latest(repre_entity): harmony.send( { "function": "PypeHarmony.setColor", @@ -125,7 +125,7 @@ class ImageSequenceLoader(load.LoaderPlugin): }) harmony.imprint( - node, {"representation": str(repre_doc["_id"])} + node, {"representation": repre_entity["id"]} ) def remove(self, container): diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_palette.py b/client/ayon_core/hosts/harmony/plugins/load/load_palette.py index f9ce888f93..79ae2fb154 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_palette.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_palette.py @@ -27,16 +27,15 @@ class ImportPaletteLoader(load.LoaderPlugin): ) def load_palette(self, context): - subset_doc = context["subset"] - repre_doc = context["representation"] - product_name = subset_doc["name"] + product_name = context["product"]["name"] + repre_entity = context["representation"] name = product_name.replace("palette", "") # Overwrite palette on disk. scene_path = harmony.send( {"function": "scene.currentProjectPath"} )["result"] - src = get_representation_path(repre_doc) + src = get_representation_path(repre_entity) dst = os.path.join( scene_path, "palette-library", @@ -68,7 +67,7 @@ class ImportPaletteLoader(load.LoaderPlugin): self.remove(container) name = self.load_palette(context) - repre_doc = context["representation"] - container["representation"] = str(repre_doc["_id"]) + repre_entity = context["representation"] + container["representation"] = repre_entity["id"] container["name"] = name harmony.imprint(name, container) diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_template.py b/client/ayon_core/hosts/harmony/plugins/load/load_template.py index e981340c68..2d9af362eb 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_template.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_template.py @@ -52,8 +52,8 @@ class TemplateLoader(load.LoaderPlugin): { "function": f"PypeHarmony.Loaders.{self_name}.loadContainer", "args": [template_path, - context["asset"]["name"], - context["subset"]["name"], + context["folder"]["name"], + context["product"]["name"], group_id] } )["result"] @@ -82,8 +82,8 @@ class TemplateLoader(load.LoaderPlugin): node = harmony.find_node_by_name(node_name, "GROUP") self_name = self.__class__.__name__ - repre_doc = context["representation"] - if is_representation_from_latest(repre_doc): + repre_entity = context["representation"] + if is_representation_from_latest(repre_entity): self._set_green(node) else: self._set_red(node) @@ -111,7 +111,7 @@ class TemplateLoader(load.LoaderPlugin): None, container["data"]) harmony.imprint( - node, {"representation": str(repre_doc["_id"])} + node, {"representation": repre_entity["id"]} ) def remove(self, container): diff --git a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py index 1b127c5bc4..59d66b7cfc 100644 --- a/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py +++ b/client/ayon_core/hosts/harmony/plugins/load/load_template_workfile.py @@ -40,7 +40,7 @@ class ImportTemplateLoader(load.LoaderPlugin): shutil.rmtree(temp_dir) - product_name = context["subset"]["name"] + product_name = context["product"]["name"] return harmony.containerise( product_name, diff --git a/client/ayon_core/hosts/harmony/plugins/publish/collect_workfile.py b/client/ayon_core/hosts/harmony/plugins/publish/collect_workfile.py index b1010cfb57..488a7c4c71 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/collect_workfile.py +++ b/client/ayon_core/hosts/harmony/plugins/publish/collect_workfile.py @@ -17,10 +17,15 @@ class CollectWorkfile(pyblish.api.ContextPlugin): """Plugin entry point.""" product_type = "workfile" basename = os.path.basename(context.data["currentFile"]) + task_entity = context.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] product_name = get_product_name( context.data["projectName"], - context.data["assetEntity"], - context.data["task"], + task_name, + task_type, context.data["hostName"], product_type, "", diff --git a/client/ayon_core/hosts/harmony/plugins/publish/help/validate_instances.xml b/client/ayon_core/hosts/harmony/plugins/publish/help/validate_instances.xml index 67ad7e2d21..8c2b523e29 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/help/validate_instances.xml +++ b/client/ayon_core/hosts/harmony/plugins/publish/help/validate_instances.xml @@ -1,25 +1,25 @@ -Subset context +Product context ## Invalid product context -Asset name found '{found}' in products, expected '{expected}'. +Folder path found '{found}' in products, expected '{expected}'. ### How to repair? -You can fix this with `Repair` button on the right. This will use '{expected}' asset name and overwrite '{found}' asset name in scene metadata. +You can fix this with `Repair` button on the right. This will use '{expected}' folder path and overwrite '{found}' folder path in scene metadata. After that restart `Publish` with a `Reload button`. -If this is unwanted, close workfile and open again, that way different asset value would be used for context information. +If this is unwanted, close workfile and open again, that way different folder value would be used for context information. ### __Detailed Info__ (optional) This might happen if you are reuse old workfile and open it in different context. -(Eg. you created product "renderCompositingDefault" from asset "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing product for "Robot" asset stayed in the workfile.) +(Eg. you created product "renderCompositingDefault" from folder "Robot' in "your_project_Robot_compositing.aep", now you opened this workfile in a context "Sloth" but existing product for "Robot" folder stayed in the workfile.) \ No newline at end of file diff --git a/client/ayon_core/hosts/harmony/plugins/publish/help/validate_scene_settings.xml b/client/ayon_core/hosts/harmony/plugins/publish/help/validate_scene_settings.xml index 36fa90456e..b645a97cb2 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/help/validate_scene_settings.xml +++ b/client/ayon_core/hosts/harmony/plugins/publish/help/validate_scene_settings.xml @@ -5,18 +5,18 @@ ## Invalid scene setting found -One of the settings in a scene doesn't match to asset settings in database. +One of the settings in a scene doesn't match to folder attributes on server. {invalid_setting_str} ### How to repair? -Change values for {invalid_keys_str} in the scene OR change them in the asset database if they are wrong there. +Change values for {invalid_keys_str} in the scene OR change them on the folder if they are wrong there. ### __Detailed Info__ (optional) -This error is shown when for example resolution in the scene doesn't match to resolution set on the asset in the database. +This error is shown when for example resolution in the scene doesn't match to resolution set on the folder on the server. Either value in the database or in the scene is wrong. diff --git a/client/ayon_core/hosts/harmony/plugins/publish/validate_instances.py b/client/ayon_core/hosts/harmony/plugins/publish/validate_instances.py index fdba834de6..1200f6266b 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/validate_instances.py +++ b/client/ayon_core/hosts/harmony/plugins/publish/validate_instances.py @@ -1,7 +1,7 @@ import pyblish.api import ayon_core.hosts.harmony.api as harmony -from ayon_core.pipeline import get_current_asset_name +from ayon_core.pipeline import get_current_folder_path from ayon_core.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -27,7 +27,7 @@ class ValidateInstanceRepair(pyblish.api.Action): # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) - folder_path = get_current_asset_name() + folder_path = get_current_folder_path() for instance in instances: data = harmony.read(instance.data["setMembers"][0]) data["folderPath"] = folder_path @@ -35,7 +35,7 @@ class ValidateInstanceRepair(pyblish.api.Action): class ValidateInstance(pyblish.api.InstancePlugin): - """Validate the instance asset is the current asset.""" + """Validate the instance folder is the current folder.""" label = "Validate Instance" hosts = ["harmony"] @@ -43,17 +43,18 @@ class ValidateInstance(pyblish.api.InstancePlugin): order = ValidateContentsOrder def process(self, instance): - instance_asset = instance.data["folderPath"] - current_asset = get_current_asset_name() + instance_folder_path = instance.data["folderPath"] + current_colder_path = get_current_folder_path() msg = ( - "Instance asset is not the same as current asset:" - f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + "Instance folder is not the same as current folder:" + f"\nInstance: {instance_folder_path}]" + f"\nCurrent: {current_colder_path}" ) formatting_data = { - "found": instance_asset, - "expected": current_asset + "found": instance_folder_path, + "expected": current_colder_path } - if instance_asset != current_asset: + if instance_folder_path != current_colder_path: raise PublishXmlValidationError(self, msg, formatting_data=formatting_data) diff --git a/client/ayon_core/hosts/harmony/plugins/publish/validate_scene_settings.py b/client/ayon_core/hosts/harmony/plugins/publish/validate_scene_settings.py index 6d46fbcd33..dc3db3b544 100644 --- a/client/ayon_core/hosts/harmony/plugins/publish/validate_scene_settings.py +++ b/client/ayon_core/hosts/harmony/plugins/publish/validate_scene_settings.py @@ -19,12 +19,12 @@ class ValidateSceneSettingsRepair(pyblish.api.Action): def process(self, context, plugin): """Repair action entry point.""" - expected = harmony.get_asset_settings() - asset_settings = _update_frames(dict.copy(expected)) - asset_settings["frameStart"] = 1 - asset_settings["frameEnd"] = asset_settings["frameEnd"] + \ - asset_settings["handleEnd"] - harmony.set_scene_settings(asset_settings) + expected = harmony.get_current_context_settings() + expected_settings = _update_frames(dict.copy(expected)) + expected_settings["frameStart"] = 1 + expected_settings["frameEnd"] = expected_settings["frameEnd"] + \ + expected_settings["handleEnd"] + harmony.set_scene_settings(expected_settings) if not os.path.exists(context.data["scenePath"]): self.log.info("correcting scene name") scene_dir = os.path.dirname(context.data["currentFile"]) @@ -56,10 +56,10 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" - # TODO 'get_asset_settings' could expect asset document as argument - # which is available on 'context.data["assetEntity"]' + # TODO 'get_current_context_settings' could expect folder entity + # as an argument which is available on 'context.data["folderEntity"]' # - the same approach can be used in 'ValidateSceneSettingsRepair' - expected_settings = harmony.get_asset_settings() + expected_settings = harmony.get_current_context_settings() self.log.info("scene settings from DB:{}".format(expected_settings)) expected_settings.pop("entityType") # not useful for the validation @@ -87,8 +87,8 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): expected_settings.pop('frameStartHandle', None) expected_settings.pop('frameEndHandle', None) - asset_name = instance.context.data['anatomyData']['asset'] - if any(re.search(pattern, asset_name) + folder_name = instance.context.data["folderPath"].rsplit("/", 1)[-1] + if any(re.search(pattern, folder_name) for pattern in self.frame_check_filter): self.log.info("Skipping frames check because of " "task name and pattern {}".format( diff --git a/client/ayon_core/hosts/hiero/api/lib.py b/client/ayon_core/hosts/hiero/api/lib.py index ef0f9edca9..c46269b532 100644 --- a/client/ayon_core/hosts/hiero/api/lib.py +++ b/client/ayon_core/hosts/hiero/api/lib.py @@ -15,12 +15,12 @@ import shutil import hiero from qtpy import QtWidgets, QtCore +import ayon_api try: from PySide import QtXml except ImportError: from PySide2 import QtXml -from ayon_core.client import get_project from ayon_core.settings import get_project_settings from ayon_core.pipeline import ( Anatomy, @@ -588,7 +588,7 @@ def imprint(track_item, data=None): Examples: data = { - 'asset': 'sq020sh0280', + 'folderPath': '/shots/sq020sh0280', 'productType': 'render', 'productName': 'productMain' } @@ -654,17 +654,17 @@ def sync_avalon_data_to_workfile(): project.setProjectRoot(active_project_root) # get project data from avalon db - project_doc = get_project(project_name) - project_data = project_doc["data"] + project_entity = ayon_api.get_project(project_name) + project_attribs = project_entity["attrib"] - log.debug("project_data: {}".format(project_data)) + log.debug("project attributes: {}".format(project_attribs)) # get format and fps property from avalon db on project - width = project_data["resolutionWidth"] - height = project_data["resolutionHeight"] - pixel_aspect = project_data["pixelAspect"] - fps = project_data['fps'] - format_name = project_data['code'] + width = project_attribs["resolutionWidth"] + height = project_attribs["resolutionHeight"] + pixel_aspect = project_attribs["pixelAspect"] + fps = project_attribs["fps"] + format_name = project_entity["code"] # create new format in hiero project format = hiero.core.Format(width, height, pixel_aspect, format_name) diff --git a/client/ayon_core/hosts/hiero/api/menu.py b/client/ayon_core/hosts/hiero/api/menu.py index ba0cbdd120..632b11c7d3 100644 --- a/client/ayon_core/hosts/hiero/api/menu.py +++ b/client/ayon_core/hosts/hiero/api/menu.py @@ -11,7 +11,7 @@ from ayon_core.tools.utils import host_tools from ayon_core.settings import get_project_settings from ayon_core.pipeline import ( get_current_project_name, - get_current_asset_name, + get_current_folder_path, get_current_task_name ) @@ -25,7 +25,7 @@ self._change_context_menu = None def get_context_label(): return "{}, {}".format( - get_current_asset_name(), + get_current_folder_path(), get_current_task_name() ) diff --git a/client/ayon_core/hosts/hiero/api/pipeline.py b/client/ayon_core/hosts/hiero/api/pipeline.py index a9ba2e4df3..327a4ae29c 100644 --- a/client/ayon_core/hosts/hiero/api/pipeline.py +++ b/client/ayon_core/hosts/hiero/api/pipeline.py @@ -101,7 +101,7 @@ def containerise(track_item, "name": str(name), "namespace": str(namespace), "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], }) if data: diff --git a/client/ayon_core/hosts/hiero/api/plugin.py b/client/ayon_core/hosts/hiero/api/plugin.py index 51601a7ee0..6a665dc9c5 100644 --- a/client/ayon_core/hosts/hiero/api/plugin.py +++ b/client/ayon_core/hosts/hiero/api/plugin.py @@ -409,8 +409,9 @@ class ClipLoader: "Cannot Load selected data, look into database " "or call your supervisor") - # inject asset data to representation dict - self._get_asset_data() + # inject folder data to representation dict + folder_entity = self.context["folder"] + self.data["folderAttributes"] = folder_entity["attrib"] log.info("__init__ self.data: `{}`".format(pformat(self.data))) log.info("__init__ options: `{}`".format(pformat(options))) @@ -424,7 +425,7 @@ class ClipLoader: self.active_sequence = lib.get_current_sequence(new=True) self.active_sequence.setFramerate( hiero.core.TimeBase.fromString( - str(self.data["assetData"]["fps"]))) + str(self.data["folderAttributes"]["fps"]))) else: self.active_sequence = lib.get_current_sequence() @@ -447,16 +448,17 @@ class ClipLoader: # create name repr = self.context["representation"] repr_cntx = repr["context"] - asset = str(repr_cntx["asset"]) - product_name = str(repr_cntx["subset"]) - representation = str(repr_cntx["representation"]) + folder_path = self.context["folder"]["path"] + folder_name = self.context["folder"]["name"] + product_name = self.context["product"]["name"] + representation = repr["name"] self.data["clip_name"] = self.clip_name_template.format(**repr_cntx) self.data["track_name"] = "_".join([product_name, representation]) - self.data["versionData"] = self.context["version"]["data"] + self.data["versionAttributes"] = self.context["version"]["attrib"] # gets file path file = get_representation_path_from_context(self.context) if not file: - repr_id = repr["_id"] + repr_id = repr["id"] log.warning( "Representation id `{}` is failing to load".format(repr_id)) return None @@ -467,11 +469,7 @@ class ClipLoader: self._fix_path_hashes() # solve project bin structure path - hierarchy = str("/".join(( - "Loader", - repr_cntx["hierarchy"].replace("\\", "/"), - asset - ))) + hierarchy = "Loader{}".format(folder_path) self.data["binPath"] = hierarchy @@ -487,16 +485,6 @@ class ClipLoader: file = file.replace(frame, "#" * padding) self.data["path"] = file - def _get_asset_data(self): - """ Get all available asset data - - joint `data` key with asset.data dict into the representation - - """ - - asset_doc = self.context["asset"] - self.data["assetData"] = asset_doc["data"] - def _make_track_item(self, source_bin_item, audio=False): """ Create track item with """ @@ -530,12 +518,13 @@ class ClipLoader: self.media_duration = int(self.media.duration()) # get handles - self.handle_start = self.data["versionData"].get("handleStart") - self.handle_end = self.data["versionData"].get("handleEnd") + version_attributes = self.data["versionAttributes"] + self.handle_start = version_attributes.get("handleStart") + self.handle_end = version_attributes.get("handleEnd") if self.handle_start is None: - self.handle_start = self.data["assetData"]["handleStart"] + self.handle_start = self.data["folderAttributes"]["handleStart"] if self.handle_end is None: - self.handle_end = self.data["assetData"]["handleEnd"] + self.handle_end = self.data["folderAttributes"]["handleEnd"] self.handle_start = int(self.handle_start) self.handle_end = int(self.handle_end) @@ -552,11 +541,11 @@ class ClipLoader: last_timeline_out = int(last_track_item.timelineOut()) + 1 self.timeline_in = last_timeline_out self.timeline_out = last_timeline_out + int( - self.data["assetData"]["clipOut"] - - self.data["assetData"]["clipIn"]) + self.data["folderAttributes"]["clipOut"] + - self.data["folderAttributes"]["clipIn"]) else: - self.timeline_in = int(self.data["assetData"]["clipIn"]) - self.timeline_out = int(self.data["assetData"]["clipOut"]) + self.timeline_in = int(self.data["folderAttributes"]["clipIn"]) + self.timeline_out = int(self.data["folderAttributes"]["clipOut"]) log.debug("__ self.timeline_in: {}".format(self.timeline_in)) log.debug("__ self.timeline_out: {}".format(self.timeline_out)) diff --git a/client/ayon_core/hosts/hiero/api/tags.py b/client/ayon_core/hosts/hiero/api/tags.py index 6491b1f384..32620aa2f5 100644 --- a/client/ayon_core/hosts/hiero/api/tags.py +++ b/client/ayon_core/hosts/hiero/api/tags.py @@ -1,9 +1,9 @@ import json import re -import os import hiero -from ayon_core.client import get_project, get_assets +import ayon_api + from ayon_core.lib import Logger from ayon_core.pipeline import get_current_project_name @@ -143,40 +143,22 @@ def add_tags_to_workfile(): # Get project task types. project_name = get_current_project_name() - project_doc = get_project(project_name) - tasks = project_doc["config"]["tasks"] + project_entity = ayon_api.get_project(project_name) + task_types = project_entity["taskType"] nks_pres_tags["[Tasks]"] = {} - log.debug("__ tasks: {}".format(tasks)) - for task_type in tasks.keys(): - nks_pres_tags["[Tasks]"][task_type.lower()] = { + log.debug("__ tasks: {}".format(task_types)) + for task_type in task_types: + task_type_name = task_type["name"] + nks_pres_tags["[Tasks]"][task_type_name.lower()] = { "editable": "1", - "note": task_type, + "note": task_type_name, "icon": "icons:TagGood.png", "metadata": { "productType": "task", - "type": task_type + "type": task_type_name } } - # Get project assets. Currently Ftrack specific to differentiate between - # asset builds and shots. - if int(os.getenv("TAG_ASSETBUILD_STARTUP", 0)) == 1: - nks_pres_tags["[AssetBuilds]"] = {} - for asset in get_assets( - project_name, fields=["name", "data.entityType"] - ): - if asset["data"]["entityType"] == "AssetBuild": - nks_pres_tags["[AssetBuilds]"][asset["name"]] = { - "editable": "1", - "note": "", - "icon": { - "path": "icons:TagActor.png" - }, - "metadata": { - "productType": "assetbuild" - } - } - # loop through tag data dict and create deep tag structure for _k, _val in nks_pres_tags.items(): # check if key is not decorated with [] so it is defined as bin diff --git a/client/ayon_core/hosts/hiero/plugins/load/load_clip.py b/client/ayon_core/hosts/hiero/plugins/load/load_clip.py index e5ef977c42..c2ff907650 100644 --- a/client/ayon_core/hosts/hiero/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/hiero/plugins/load/load_clip.py @@ -1,11 +1,6 @@ -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id -) -from ayon_core.pipeline import ( - get_representation_path, - get_current_project_name, -) +import ayon_api + +from ayon_core.pipeline import get_representation_path from ayon_core.lib.transcoding import ( VIDEO_EXTENSIONS, IMAGE_EXTENSIONS @@ -101,10 +96,10 @@ class LoadClip(phiero.SequenceLoader): path = self.filepath_from_context(context) track_item = phiero.ClipLoader(self, context, path, **options).load() namespace = namespace or track_item.name() - version = context['version'] - version_data = version.get("data", {}) - version_name = version.get("name", None) - colorspace = version_data.get("colorspace", None) + version_entity = context["version"] + version_attributes = version_entity["attrib"] + version_name = version_entity["version"] + colorspace = version_attributes.get("colorSpace") object_name = self.clip_name_template.format( **context["representation"]["context"]) @@ -119,11 +114,11 @@ class LoadClip(phiero.SequenceLoader): ] # move all version data keys to tag data - data_imprint = {} - for key in add_keys: - data_imprint.update({ - key: version_data.get(key, str(None)) - }) + data_imprint = { + key: version_attributes.get(key, str(None)) + for key in add_keys + + } # add variables related to version context data_imprint.update({ @@ -133,7 +128,9 @@ class LoadClip(phiero.SequenceLoader): }) # update color of clip regarding the version order - self.set_item_color(track_item, version) + self.set_item_color( + context["project"]["name"], track_item, version_entity + ) # deal with multiselection self.multiselection(track_item) @@ -152,19 +149,21 @@ class LoadClip(phiero.SequenceLoader): def update(self, container, context): """ Updating previously loaded clips """ - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] + # load clip to timeline and get main variables - name = container['name'] - namespace = container['namespace'] + name = container["name"] + namespace = container["namespace"] track_item = phiero.get_track_items( track_item_name=namespace).pop() - version_data = version_doc.get("data", {}) - version_name = version_doc.get("name", None) - colorspace = version_data.get("colorspace", None) + version_attributes = version_entity["attrib"] + version_name = version_entity["version"] + colorspace = version_attributes.get("colorSpace") object_name = "{}_{}".format(name, namespace) - file = get_representation_path(repre_doc).replace("\\", "/") + + file = get_representation_path(repre_entity).replace("\\", "/") clip = track_item.source() # reconnect media to new path @@ -174,29 +173,35 @@ class LoadClip(phiero.SequenceLoader): if colorspace: clip.setSourceMediaColourTransform(colorspace) - # add additional metadata from the version to imprint Avalon knob - add_keys = [ - "frameStart", "frameEnd", "source", "author", - "fps", "handleStart", "handleEnd" - ] + # add additional metadata from the version to imprint metadata knob # move all version data keys to tag data data_imprint = {} - for key in add_keys: + for key in [ + "frameStart", + "frameEnd", + "source", + "author", + "fps", + "handleStart", + "handleEnd", + ]: data_imprint.update({ - key: version_data.get(key, str(None)) + key: version_attributes.get(key, str(None)) }) # add variables related to version context data_imprint.update({ - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "version": version_name, "colorspace": colorspace, "objectName": object_name }) # update color of clip regarding the version order - self.set_item_color(track_item, version_doc) + self.set_item_color( + context["project"]["name"], track_item, version_entity + ) return phiero.update_container(track_item, data_imprint) @@ -219,14 +224,13 @@ class LoadClip(phiero.SequenceLoader): cls.sequence = cls.track.parent() @classmethod - def set_item_color(cls, track_item, version_doc): - project_name = get_current_project_name() - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + def set_item_color(cls, project_name, track_item, version_entity): + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) clip = track_item.source() # set clip colour - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: clip.binItem().setColor(cls.clip_color_last) else: clip.binItem().setColor(cls.clip_color) diff --git a/client/ayon_core/hosts/hiero/plugins/load/load_effects.py b/client/ayon_core/hosts/hiero/plugins/load/load_effects.py index 9a5e659451..521a7c4494 100644 --- a/client/ayon_core/hosts/hiero/plugins/load/load_effects.py +++ b/client/ayon_core/hosts/hiero/plugins/load/load_effects.py @@ -2,15 +2,10 @@ import json from collections import OrderedDict import six -from ayon_core.client import ( - get_version_by_id -) - from ayon_core.pipeline import ( AVALON_CONTAINER_ID, load, get_representation_path, - get_current_project_name ) from ayon_core.hosts.hiero import api as phiero from ayon_core.lib import Logger @@ -37,7 +32,7 @@ class LoadEffects(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): Folder name. data (dict): compulsory attribute > not used Returns: @@ -48,10 +43,10 @@ class LoadEffects(load.LoaderPlugin): active_sequence, "Loaded_{}".format(name)) # get main variables - namespace = namespace or context["asset"]["name"] + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) - clip_in = context["asset"]["data"]["clipIn"] - clip_out = context["asset"]["data"]["clipOut"] + clip_in = context["folder"]["attrib"]["clipIn"] + clip_out = context["folder"]["attrib"]["clipOut"] data_imprint = { "objectName": object_name, @@ -160,19 +155,19 @@ class LoadEffects(load.LoaderPlugin): def update(self, container, context): """ Updating previously loaded effects """ - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] active_track = container["_item"] - file = get_representation_path(repre_doc).replace("\\", "/") + file = get_representation_path(repre_entity).replace("\\", "/") # get main variables name = container['name'] namespace = container['namespace'] # get timeline in out data - version_data = version_doc["data"] - clip_in = version_data["clipIn"] - clip_out = version_data["clipOut"] + version_attributes = version_entity["attrib"] + clip_in = version_attributes["clipIn"] + clip_out = version_attributes["clipOut"] object_name = "{}_{}".format(name, namespace) @@ -197,7 +192,7 @@ class LoadEffects(load.LoaderPlugin): data_imprint = { "objectName": object_name, "name": name, - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "children_names": [] } @@ -298,7 +293,7 @@ class LoadEffects(load.LoaderPlugin): "name": str(name), "namespace": str(namespace), "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } } diff --git a/client/ayon_core/hosts/hiero/plugins/publish/collect_frame_tag_instances.py b/client/ayon_core/hosts/hiero/plugins/publish/collect_frame_tag_instances.py index d73b5d4667..0e5d849b78 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/collect_frame_tag_instances.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/collect_frame_tag_instances.py @@ -5,8 +5,6 @@ import json import pyblish.api -from ayon_core.client import get_asset_name_identifier - class CollectFrameTagInstances(pyblish.api.ContextPlugin): """Collect frames from tags. @@ -104,8 +102,7 @@ class CollectFrameTagInstances(pyblish.api.ContextPlugin): # first collect all available product tag frames product_data = {} - context_asset_doc = context.data["assetEntity"] - context_folder_path = get_asset_name_identifier(context_asset_doc) + context_folder_path = context.data["folderEntity"]["path"] for tag_data in sequence_tags: frame = int(tag_data["start"]) diff --git a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py index 911b96c280..d921f37934 100644 --- a/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/hiero/plugins/publish/precollect_instances.py @@ -85,7 +85,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset, asset_name = self._get_asset_data(tag_data) + asset, asset_name = self._get_folder_data(tag_data) product_name = tag_data.get("productName") if product_name is None: @@ -242,13 +242,13 @@ class PrecollectInstances(pyblish.api.ContextPlugin): self.log.debug( "_ instance.data: {}".format(pformat(instance.data))) - def _get_asset_data(self, data): + def _get_folder_data(self, data): folder_path = data.pop("folderPath", None) if data.get("asset_name"): - asset_name = data["asset_name"] + folder_name = data["asset_name"] else: - asset_name = data["asset"] + folder_name = data["asset"] # backward compatibility for clip tags # which are missing folderPath key @@ -257,10 +257,10 @@ class PrecollectInstances(pyblish.api.ContextPlugin): hierarchy_path = data["hierarchy"] folder_path = "/{}/{}".format( hierarchy_path, - asset_name + folder_name ) - return folder_path, asset_name + return folder_path, folder_name def create_audio_instance(self, context, **data): product_name = "audioMain" diff --git a/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py b/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py deleted file mode 100644 index 96d471115a..0000000000 --- a/client/ayon_core/hosts/hiero/plugins/publish_old_workflow/collect_assetbuilds.py +++ /dev/null @@ -1,64 +0,0 @@ -from pyblish import api - -from ayon_core.client import get_assets, get_asset_name_identifier - - -class CollectAssetBuilds(api.ContextPlugin): - """Collect asset from tags. - - Tag is expected to have name of the asset and metadata: - { - "productType": "assetbuild" - } - """ - - # Run just after CollectClip - order = api.CollectorOrder + 0.02 - label = "Collect AssetBuilds" - hosts = ["hiero"] - - def process(self, context): - project_name = context.data["projectName"] - asset_builds = {} - for asset_doc in get_assets(project_name): - if asset_doc["data"].get("entityType") != "AssetBuild": - continue - - asset_name = get_asset_name_identifier(asset_doc) - self.log.debug("Found \"{}\" in database.".format(asset_doc)) - asset_builds[asset_name] = asset_doc - - for instance in context: - if instance.data["productType"] != "clip": - continue - - # Exclude non-tagged instances. - tagged = False - asset_names = [] - - for tag in instance.data["tags"]: - t_metadata = dict(tag.metadata()) - t_product_type = t_metadata.get("tag.productType") - if t_product_type is None: - t_product_type = t_metadata.get("tag.family", "") - - if t_product_type.lower() == "assetbuild": - asset_names.append(tag["name"]) - tagged = True - - if not tagged: - self.log.debug( - "Skipping \"{}\" because its not tagged with " - "\"assetbuild\"".format(instance) - ) - continue - - # Collect asset builds. - data = {"assetbuilds": []} - for name in asset_names: - data["assetbuilds"].append(asset_builds[name]) - self.log.debug( - "Found asset builds: {}".format(data["assetbuilds"]) - ) - - instance.data.update(data) diff --git a/client/ayon_core/hosts/houdini/api/creator_node_shelves.py b/client/ayon_core/hosts/houdini/api/creator_node_shelves.py index 57fdef753a..6e48cb375b 100644 --- a/client/ayon_core/hosts/houdini/api/creator_node_shelves.py +++ b/client/ayon_core/hosts/houdini/api/creator_node_shelves.py @@ -12,7 +12,8 @@ import tempfile import logging import os -from ayon_core.client import get_asset_by_name +import ayon_api + from ayon_core.pipeline import registered_host from ayon_core.pipeline.create import CreateContext from ayon_core.resources import get_ayon_icon_filepath @@ -90,13 +91,19 @@ def create_interactive(creator_identifier, **kwargs): pane = stateutils.activePane(kwargs) if isinstance(pane, hou.NetworkEditor): pwd = pane.pwd() + project_name = context.get_current_project_name(), + folder_path = context.get_current_folder_path() + task_name = context.get_current_task_name() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = creator.get_product_name( project_name=context.get_current_project_name(), - asset_doc=get_asset_by_name( - project_name=context.get_current_project_name(), - asset_name=context.get_current_asset_name() - ), - task_name=context.get_current_task_name(), + folder_entity=folder_entity, + task_entity=task_entity, variant=variant, host_name=context.host_name, ) diff --git a/client/ayon_core/hosts/houdini/api/lib.py b/client/ayon_core/hosts/houdini/api/lib.py index 9db055779d..681052a44d 100644 --- a/client/ayon_core/hosts/houdini/api/lib.py +++ b/client/ayon_core/hosts/houdini/api/lib.py @@ -9,21 +9,21 @@ import json from contextlib import contextmanager import six +import ayon_api from ayon_core.lib import StringTemplate -from ayon_core.client import get_project, get_asset_by_name from ayon_core.settings import get_current_project_settings from ayon_core.pipeline import ( Anatomy, get_current_project_name, - get_current_asset_name, + get_current_folder_path, registered_host, get_current_context, get_current_host_name, ) from ayon_core.pipeline.create import CreateContext from ayon_core.pipeline.template_data import get_template_data -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.tools.utils import PopupUpdateKeys, SimplePopup from ayon_core.tools.utils.host_tools import get_tool_by_name @@ -36,12 +36,12 @@ log = logging.getLogger(__name__) JSON_PREFIX = "JSON:::" -def get_asset_fps(asset_doc=None): - """Return current asset fps.""" +def get_folder_fps(folder_entity=None): + """Return current folder fps.""" - if asset_doc is None: - asset_doc = get_current_project_asset(fields=["data.fps"]) - return asset_doc["data"]["fps"] + if folder_entity is None: + folder_entity = get_current_project_folder(fields=["attrib.fps"]) + return folder_entity["attrib"]["fps"] def set_id(node, unique_id, overwrite=False): @@ -68,7 +68,7 @@ def get_id(node): return node.parm("id") -def generate_ids(nodes, asset_id=None): +def generate_ids(nodes, folder_id=None): """Returns new unique ids for the given nodes. Note: This does not assign the new ids, it only generates the values. @@ -85,28 +85,29 @@ def generate_ids(nodes, asset_id=None): Args: nodes (list): List of nodes. - asset_id (str or bson.ObjectId): The database id for the *asset* to - generate for. When None provided the current asset in the - active session is used. + folder_id (str): Folder id . Use current folder id if is ``None``. Returns: list: A list of (node, id) tuples. """ - if asset_id is None: + if folder_id is None: project_name = get_current_project_name() - asset_name = get_current_asset_name() - # Get the asset ID from the database for the asset of current context - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) + folder_path = get_current_folder_path() + # Get folder id of current context folder + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} + ) + if not folder_entity: + raise ValueError("No current folder is set.") - assert asset_doc, "No current asset found in Session" - asset_id = asset_doc['_id'] + folder_id = folder_entity["id"] node_ids = [] for node in nodes: _, uid = str(uuid.uuid4()).rsplit("-", 1) - unique_id = "{}:{}".format(asset_id, uid) + unique_id = "{}:{}".format(folder_id, uid) node_ids.append((node, unique_id)) return node_ids @@ -199,7 +200,7 @@ def validate_fps(): """ - fps = get_asset_fps() + fps = get_folder_fps() current_fps = hou.fps() # returns float if current_fps != fps: @@ -526,28 +527,27 @@ def maintained_selection(): def reset_framerange(): - """Set frame range and FPS to current asset""" + """Set frame range and FPS to current folder.""" - # Get asset data project_name = get_current_project_name() - asset_name = get_current_asset_name() - # Get the asset ID from the database for the asset of current context - asset_doc = get_asset_by_name(project_name, asset_name) - asset_data = asset_doc["data"] + folder_path = get_current_folder_path() + + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + folder_attributes = folder_entity["attrib"] # Get FPS - fps = get_asset_fps(asset_doc) + fps = get_folder_fps(folder_entity) # Get Start and End Frames - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") if frame_start is None or frame_end is None: - log.warning("No edit information found for %s" % asset_name) + log.warning("No edit information found for '{}'".format(folder_path)) return - handle_start = asset_data.get("handleStart", 0) - handle_end = asset_data.get("handleEnd", 0) + handle_start = folder_attributes.get("handleStart", 0) + handle_end = folder_attributes.get("handleEnd", 0) frame_start -= int(handle_start) frame_end += int(handle_end) @@ -641,7 +641,7 @@ def get_frame_data(node, log=None): log.info( "Node '{}' has 'Render current frame' set.\n" - "Asset Handles are ignored.\n" + "Folder Handles are ignored.\n" "frameStart and frameEnd are set to the " "current frame.".format(node.path()) ) @@ -780,31 +780,43 @@ def get_output_children(output_node, include_sops=True): return out_list -def get_resolution_from_doc(doc): - """Get resolution from the given asset document. """ +def get_resolution_from_folder(folder_entity): + """Get resolution from the given folder entity. - if not doc or "data" not in doc: - print("Entered document is not valid. \"{}\"".format(str(doc))) + Args: + folder_entity (dict[str, Any]): Folder entity. + + Returns: + Union[Tuple[int, int], None]: Resolution width and height. + + """ + if not folder_entity or "attrib" not in folder_entity: + print("Entered folder is not valid. \"{}\"".format( + str(folder_entity) + )) return None - resolution_width = doc["data"].get("resolutionWidth") - resolution_height = doc["data"].get("resolutionHeight") + folder_attributes = folder_entity["attrib"] + resolution_width = folder_attributes.get("resolutionWidth") + resolution_height = folder_attributes.get("resolutionHeight") # Make sure both width and height are set if resolution_width is None or resolution_height is None: - print("No resolution information found for \"{}\"".format(doc["name"])) + print("No resolution information found for '{}'".format( + folder_entity["path"] + )) return None return int(resolution_width), int(resolution_height) -def set_camera_resolution(camera, asset_doc=None): - """Apply resolution to camera from asset document of the publish""" +def set_camera_resolution(camera, folder_entity=None): + """Apply resolution to camera from folder entity of the publish""" - if not asset_doc: - asset_doc = get_current_project_asset() + if not folder_entity: + folder_entity = get_current_project_folder() - resolution = get_resolution_from_doc(asset_doc) + resolution = get_resolution_from_folder(folder_entity) if resolution: print("Setting camera resolution: {} -> {}x{}".format( @@ -827,41 +839,47 @@ def get_camera_from_container(container): return cameras[0] -def get_current_context_template_data_with_asset_data(): - """ - TODOs: - Support both 'assetData' and 'folderData' in future. +def get_current_context_template_data_with_folder_attrs(): """ + Output contains 'folderAttributes' key with folder attribute values. + + Returns: + dict[str, Any]: Template data to fill templates. + + """ context = get_current_context() project_name = context["project_name"] - asset_name = context["folder_path"] + folder_path = context["folder_path"] task_name = context["task_name"] host_name = get_current_host_name() - anatomy = Anatomy(project_name) - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) + project_entity = ayon_api.get_project(project_name) + anatomy = Anatomy(project_name, project_entity=project_entity) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) # get context specific vars - asset_data = asset_doc["data"] + folder_attributes = folder_entity["attrib"] # compute `frameStartHandle` and `frameEndHandle` - frame_start = asset_data.get("frameStart") - frame_end = asset_data.get("frameEnd") - handle_start = asset_data.get("handleStart") - handle_end = asset_data.get("handleEnd") + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") + handle_start = folder_attributes.get("handleStart") + handle_end = folder_attributes.get("handleEnd") if frame_start is not None and handle_start is not None: - asset_data["frameStartHandle"] = frame_start - handle_start + folder_attributes["frameStartHandle"] = frame_start - handle_start if frame_end is not None and handle_end is not None: - asset_data["frameEndHandle"] = frame_end + handle_end + folder_attributes["frameEndHandle"] = frame_end + handle_end template_data = get_template_data( - project_doc, asset_doc, task_name, host_name + project_entity, folder_entity, task_entity, host_name ) template_data["root"] = anatomy.roots - template_data["assetData"] = asset_data + template_data["folderAttributes"] = folder_attributes return template_data @@ -885,7 +903,7 @@ def get_context_var_changes(): return houdini_vars_to_update # Get Template data - template_data = get_current_context_template_data_with_asset_data() + template_data = get_current_context_template_data_with_folder_attrs() # Set Houdini Vars for item in houdini_vars: @@ -917,7 +935,7 @@ def get_context_var_changes(): def update_houdini_vars_context(): - """Update asset context variables""" + """Update folder context variables""" for var, (_old, new, is_directory) in get_context_var_changes().items(): if is_directory: @@ -936,7 +954,7 @@ def update_houdini_vars_context(): def update_houdini_vars_context_dialog(): - """Show pop-up to update asset context variables""" + """Show pop-up to update folder context variables""" update_vars = get_context_var_changes() if not update_vars: # Nothing to change @@ -952,7 +970,7 @@ def update_houdini_vars_context_dialog(): parent = hou.ui.mainQtWindow() dialog = SimplePopup(parent=parent) dialog.setModal(True) - dialog.setWindowTitle("Houdini scene has outdated asset variables") + dialog.setWindowTitle("Houdini scene has outdated folder variables") dialog.set_message(message) dialog.set_button_text("Fix") diff --git a/client/ayon_core/hosts/houdini/api/pipeline.py b/client/ayon_core/hosts/houdini/api/pipeline.py index cbc94a2408..d5144200cf 100644 --- a/client/ayon_core/hosts/houdini/api/pipeline.py +++ b/client/ayon_core/hosts/houdini/api/pipeline.py @@ -68,7 +68,7 @@ class HoudiniHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): self._has_been_setup = True - # Set asset settings for the empty scene directly after launch of + # Set folder settings for the empty scene directly after launch of # Houdini so it initializes into the correct scene FPS, # Frame Range, etc. # TODO: make sure this doesn't trigger when @@ -235,7 +235,7 @@ def containerise(name, "name": name, "namespace": namespace, "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } lib.imprint(container, data) @@ -338,7 +338,7 @@ def on_open(): lib.update_houdini_vars_context_dialog() # Validate FPS after update_task_from_path to - # ensure it is using correct FPS for the asset + # ensure it is using correct FPS for the folder lib.validate_fps() if any_outdated_containers(): @@ -388,7 +388,7 @@ def on_new(): def _set_context_settings(): """Apply the project settings from the project definition - Settings can be overwritten by an asset if the asset.data contains + Settings can be overwritten by a folder if the folder.attrib contains any information regarding those settings. Examples of settings: diff --git a/client/ayon_core/hosts/houdini/api/plugin.py b/client/ayon_core/hosts/houdini/api/plugin.py index 13cf3c9949..0809f4e566 100644 --- a/client/ayon_core/hosts/houdini/api/plugin.py +++ b/client/ayon_core/hosts/houdini/api/plugin.py @@ -145,13 +145,13 @@ class HoudiniCreatorBase(object): @staticmethod def create_instance_node( - asset_name, node_name, parent, node_type="geometry" + folder_path, node_name, parent, node_type="geometry" ): # type: (str, str, str) -> hou.Node """Create node representing instance. Arguments: - asset_name (str): Asset name. + folder_path (str): Folder path. node_name (str): Name of the new node. parent (str): Name of the parent node. node_type (str, optional): Type of the node. @@ -186,10 +186,10 @@ class HoudiniCreator(NewCreator, HoudiniCreatorBase): if node_type is None: node_type = "geometry" - asset_name = instance_data["folderPath"] + folder_path = instance_data["folderPath"] instance_node = self.create_instance_node( - asset_name, product_name, "/out", node_type) + folder_path, product_name, "/out", node_type) self.customize_node_look(instance_node) diff --git a/client/ayon_core/hosts/houdini/api/shelves.py b/client/ayon_core/hosts/houdini/api/shelves.py index b0f5af839e..b178139020 100644 --- a/client/ayon_core/hosts/houdini/api/shelves.py +++ b/client/ayon_core/hosts/houdini/api/shelves.py @@ -10,7 +10,7 @@ from ayon_core.lib import StringTemplate import hou -from .lib import get_current_context_template_data_with_asset_data +from .lib import get_current_context_template_data_with_folder_attrs log = logging.getLogger("ayon_core.hosts.houdini.shelves") @@ -31,7 +31,7 @@ def generate_shelves(): return # Get Template data - template_data = get_current_context_template_data_with_asset_data() + template_data = get_current_context_template_data_with_folder_attrs() for config in shelves_configs: selected_option = config["options"] diff --git a/client/ayon_core/hosts/houdini/api/usd.py b/client/ayon_core/hosts/houdini/api/usd.py index e9c02a0307..443a52bc37 100644 --- a/client/ayon_core/hosts/houdini/api/usd.py +++ b/client/ayon_core/hosts/houdini/api/usd.py @@ -3,12 +3,13 @@ import contextlib import logging +import ayon_api from qtpy import QtWidgets, QtCore, QtGui from ayon_core import style -from ayon_core.client import get_asset_by_name from ayon_core.pipeline import get_current_project_name -from ayon_core.tools.utils.assets_widget import SingleSelectAssetsWidget +from ayon_core.tools.utils import PlaceholderLineEdit, RefreshButton +from ayon_core.tools.ayon_utils.widgets import SimpleFoldersWidget from pxr import Sdf @@ -16,77 +17,110 @@ from pxr import Sdf log = logging.getLogger(__name__) -class SelectAssetDialog(QtWidgets.QWidget): - """Frameless assets dialog to select asset with double click. +class SelectFolderDialog(QtWidgets.QWidget): + """Frameless folders dialog to select folder with double click. Args: - parm: Parameter where selected asset name is set. + parm: Parameter where selected folder path is set. """ def __init__(self, parm): - self.setWindowTitle("Pick Asset") + self.setWindowTitle("Pick Folder") self.setWindowFlags(QtCore.Qt.FramelessWindowHint | QtCore.Qt.Popup) - assets_widget = SingleSelectAssetsWidget(self) - assets_widget.set_project_name(get_current_project_name(), False) + header_widget = QtWidgets.QWidget(self) + + filter_input = PlaceholderLineEdit(header_widget) + filter_input.setPlaceholderText("Filter folders..") + + refresh_btn = RefreshButton(self) + + header_layout = QtWidgets.QHBoxLayout(header_widget) + header_layout.setContentsMargins(0, 0, 0, 0) + header_layout.addWidget(filter_input) + header_layout.addWidget(refresh_btn) + + for widget in ( + refresh_btn, + filter_input, + ): + size_policy = widget.sizePolicy() + size_policy.setVerticalPolicy( + QtWidgets.QSizePolicy.MinimumExpanding) + widget.setSizePolicy(size_policy) + + folders_widget = SimpleFoldersWidget(self) + folders_widget.set_project_name(get_current_project_name()) layout = QtWidgets.QHBoxLayout(self) - layout.addWidget(assets_widget) + layout.addWidget(header_widget, 0) + layout.addWidget(folders_widget, 1) - assets_widget.double_clicked.connect(self._set_parameter) - self._assets_widget = assets_widget + folders_widget.double_clicked.connect(self._set_parameter) + filter_input.textChanged.connect(self._on_filter_change) + refresh_btn.clicked.connect(self._on_refresh_clicked) + + self._folders_widget = folders_widget self._parm = parm + def _on_refresh_clicked(self): + self._folders_widget.refresh() + + def _on_filter_change(self, text): + self._folders_widget.set_name_filter(text) + def _set_parameter(self): - name = self._assets_widget.get_selected_asset_name() - self._parm.set(name) + folder_path = self._folders_widget.get_selected_folder_path() + self._parm.set(folder_path) self.close() def _on_show(self): pos = QtGui.QCursor.pos() - # Select the current asset if there is any + # Select the current folder if there is any select_id = None - name = self._parm.eval() - if name: + folder_path = self._parm.eval() + if folder_path: project_name = get_current_project_name() - db_asset = get_asset_by_name(project_name, name, fields=["_id"]) - if db_asset: - select_id = db_asset["_id"] + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} + ) + if folder_entity: + select_id = folder_entity["id"] # Set stylesheet self.setStyleSheet(style.load_stylesheet()) - # Refresh assets (is threaded) - self._assets_widget.refresh() - # Select asset - must be done after refresh + # Refresh folders (is threaded) + self._folders_widget.refresh() + # Select folder - must be done after refresh if select_id is not None: - self._assets_widget.select_asset(select_id) + self._folders_widget.set_selected_folder(select_id) # Show cursor (top right of window) near cursor self.resize(250, 400) self.move(self.mapFromGlobal(pos) - QtCore.QPoint(self.width(), 0)) def showEvent(self, event): - super(SelectAssetDialog, self).showEvent(event) + super(SelectFolderDialog, self).showEvent(event) self._on_show() -def pick_asset(node): - """Show a user interface to select an Asset in the project +def pick_folder(node): + """Show a user interface to select an Folder in the project - When double clicking an asset it will set the Asset value in the - 'asset' parameter. + When double clicking an folder it will set the Folder value in the + 'folderPath' parameter. """ - parm = node.parm("asset_name") + parm = node.parm("folderPath") if not parm: - log.error("Node has no 'asset' parameter: %s", node) + log.error("Node has no 'folderPath' parameter: %s", node) return # Construct a frameless popup so it automatically # closes when clicked outside of it. global tool - tool = SelectAssetDialog(parm) + tool = SelectFolderDialog(parm) tool.show() diff --git a/client/ayon_core/hosts/houdini/plugins/create/convert_legacy.py b/client/ayon_core/hosts/houdini/plugins/create/convert_legacy.py index 008187d9c8..1a4761172a 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/houdini/plugins/create/convert_legacy.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """Converter for legacy Houdini products.""" -from ayon_core.pipeline.create.creator_plugins import SubsetConvertorPlugin +from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin from ayon_core.hosts.houdini.api.lib import imprint -class HoudiniLegacyConvertor(SubsetConvertorPlugin): +class HoudiniLegacyConvertor(ProductConvertorPlugin): """Find and convert any legacy products in the scene. This Converter will find all legacy products in the scene and will diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py index 994977de7d..c16c95a270 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_hda.py @@ -1,9 +1,7 @@ # -*- coding: utf-8 -*- """Creator plugin for creating publishable Houdini Digital Assets.""" -from ayon_core.client import ( - get_asset_by_name, - get_subsets, -) +import ayon_api + from ayon_core.hosts.houdini.api import plugin import hou @@ -17,25 +15,25 @@ class CreateHDA(plugin.HoudiniCreator): icon = "gears" maintain_selection = False - def _check_existing(self, asset_name, product_name): + def _check_existing(self, folder_path, product_name): # type: (str) -> bool """Check if existing product name versions already exists.""" # Get all products of the current folder project_name = self.project_name - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["_id"] + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} ) - subset_docs = get_subsets( - project_name, asset_ids=[asset_doc["_id"]], fields=["name"] + product_entities = ayon_api.get_products( + project_name, folder_ids={folder_entity["id"]}, fields={"name"} ) existing_product_names_low = { - subset_doc["name"].lower() - for subset_doc in subset_docs + product_entity["name"].lower() + for product_entity in product_entities } return product_name.lower() in existing_product_names_low def create_instance_node( - self, asset_name, node_name, parent, node_type="geometry" + self, folder_path, node_name, parent, node_type="geometry" ): parent_node = hou.node("/obj") @@ -62,7 +60,7 @@ class CreateHDA(plugin.HoudiniCreator): hda_file_name="$HIP/{}.hda".format(node_name) ) hda_node.layoutChildren() - elif self._check_existing(asset_name, node_name): + elif self._check_existing(folder_path, node_name): raise plugin.OpenPypeCreatorError( ("product {} is already published with different HDA" "definition.").format(node_name)) diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_staticmesh.py b/client/ayon_core/hosts/houdini/plugins/create/create_staticmesh.py index bc8a2507cd..3271107c6e 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_staticmesh.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_staticmesh.py @@ -88,16 +88,27 @@ class CreateStaticMesh(plugin.HoudiniCreator): return attrs + [createsubnetroot, vcformat, convert_units] def get_dynamic_data( - self, project_name, asset_doc, task_name, variant, host_name, instance + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ): """ The default prodcut name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ dynamic_data = super(CreateStaticMesh, self).get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) - dynamic_data["asset"] = asset_doc["name"] + dynamic_data["asset"] = folder_entity["name"] return dynamic_data def get_selection(self): diff --git a/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py b/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py index 631ef6ce77..a958509e25 100644 --- a/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/houdini/plugins/create/create_workfile.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +import ayon_api + from ayon_core.hosts.houdini.api import plugin from ayon_core.hosts.houdini.api.lib import read, imprint from ayon_core.hosts.houdini.api.pipeline import CONTEXT_CONTAINER from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.client import get_asset_by_name import hou @@ -26,26 +27,31 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): ), None) project_name = self.project_name - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.host_name if current_instance is None: - current_instance_asset = None + current_folder_path = None else: - current_instance_asset = current_instance["folderPath"] + current_folder_path = current_instance["folderPath"] if current_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": variant, } @@ -53,8 +59,8 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): data.update( self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, current_instance) @@ -65,19 +71,24 @@ class CreateWorkfile(plugin.HoudiniCreatorBase, AutoCreator): ) self._add_instance_to_context(current_instance) elif ( - current_instance_asset != asset_name + current_folder_path != folder_path or current_instance["task"] != task_name ): # Update instance context if is not the same - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) - current_instance["folderPath"] = asset_name + current_instance["folderPath"] = folder_path current_instance["task"] = task_name current_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/houdini/plugins/inventory/set_camera_resolution.py b/client/ayon_core/hosts/houdini/plugins/inventory/set_camera_resolution.py index dadb80469a..b813f82e2e 100644 --- a/client/ayon_core/hosts/houdini/plugins/inventory/set_camera_resolution.py +++ b/client/ayon_core/hosts/houdini/plugins/inventory/set_camera_resolution.py @@ -3,7 +3,7 @@ from ayon_core.hosts.houdini.api.lib import ( get_camera_from_container, set_camera_resolution ) -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder class SetCameraResolution(InventoryAction): @@ -19,8 +19,8 @@ class SetCameraResolution(InventoryAction): ) def process(self, containers): - asset_doc = get_current_project_asset() + folder_entity = get_current_project_folder() for container in containers: node = container["node"] camera = get_camera_from_container(node) - set_camera_resolution(camera, asset_doc) + set_camera_resolution(camera, folder_entity) diff --git a/client/ayon_core/hosts/houdini/plugins/load/actions.py b/client/ayon_core/hosts/houdini/plugins/load/actions.py index 2cffa565b1..049869be25 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/actions.py +++ b/client/ayon_core/hosts/houdini/plugins/load/actions.py @@ -26,11 +26,10 @@ class SetFrameRangeLoader(load.LoaderPlugin): import hou - version = context["version"] - version_data = version.get("data", {}) + version_attributes = context["version"]["attrib"] - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + start = version_attributes.get("frameStart") + end = version_attributes.get("frameEnd") if start is None or end is None: print( @@ -64,11 +63,10 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): import hou - version = context["version"] - version_data = version.get("data", {}) + version_attributes = context["version"]["attrib"] - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + start = version_attributes.get("frameStart") + end = version_attributes.get("frameEnd") if start is None or end is None: print( diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py b/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py index 5e138cde83..5235923b4c 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_alembic.py @@ -28,7 +28,7 @@ class AbcLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node @@ -82,7 +82,7 @@ class AbcLoader(load.LoaderPlugin): ) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] try: alembic_node = next( @@ -93,13 +93,13 @@ class AbcLoader(load.LoaderPlugin): return # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") alembic_node.setParms({"fileName": file_path}) # Update attribute - node.setParms({"representation": str(repre_doc["_id"])}) + node.setParms({"representation": repre_entity["id"]}) def remove(self, container): diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py b/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py index 0d505806ff..6585df3f17 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_alembic_archive.py @@ -29,14 +29,14 @@ class AbcArchiveLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create an Alembic archive node node = obj.createNode("alembicarchive", node_name=node_name) node.moveToGoodPosition() - # TODO: add FPS of project / asset + # TODO: add FPS of project / folder node.setParms({"fileName": file_path, "channelRef": True}) @@ -56,16 +56,16 @@ class AbcArchiveLoader(load.LoaderPlugin): suffix="") def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") # Update attributes node.setParms({"fileName": file_path, - "representation": str(repre_doc["_id"])}) + "representation": repre_entity["id"]}) # Rebuild node.parm("buildHierarchy").pressButton() diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_ass.py b/client/ayon_core/hosts/houdini/plugins/load/load_ass.py index 396eb3a9f7..628b5b2f34 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_ass.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_ass.py @@ -25,7 +25,7 @@ class AssLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node @@ -50,12 +50,12 @@ class AssLoader(load.LoaderPlugin): def update(self, container, context): # Update the file path - repre_doc = context["representation"] + repre_entity = context["representation"] procedural = container["node"] - procedural.setParms({"ar_filename": self.format_path(repre_doc)}) + procedural.setParms({"ar_filename": self.format_path(repre_entity)}) # Update attribute - procedural.setParms({"representation": str(repre_doc["_id"])}) + procedural.setParms({"representation": repre_entity["id"]}) def remove(self, container): node = container["node"] diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_bgeo.py b/client/ayon_core/hosts/houdini/plugins/load/load_bgeo.py index 4817e40961..f02067db75 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_bgeo.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_bgeo.py @@ -29,7 +29,7 @@ class BgeoLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node @@ -83,7 +83,7 @@ class BgeoLoader(load.LoaderPlugin): return filename def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] try: file_node = next( @@ -94,13 +94,13 @@ class BgeoLoader(load.LoaderPlugin): return # Update the file path - file_path = get_representation_path(repre_doc) - file_path = self.format_path(file_path, repre_doc) + file_path = get_representation_path(repre_entity) + file_path = self.format_path(file_path, repre_entity) file_node.setParms({"file": file_path}) # Update attribute - node.setParms({"representation": str(repre_doc["_id"])}) + node.setParms({"representation": repre_entity["id"]}) def remove(self, container): diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_camera.py b/client/ayon_core/hosts/houdini/plugins/load/load_camera.py index 6f6560facc..50fc7f4eb6 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_camera.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_camera.py @@ -104,13 +104,13 @@ class CameraLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a archive node node = self.create_and_connect(obj, "alembicarchive", node_name) - # TODO: add FPS of project / asset + # TODO: add FPS of project / folder node.setParms({"fileName": file_path, "channelRef": True}) # Apply some magic @@ -122,7 +122,7 @@ class CameraLoader(load.LoaderPlugin): camera = get_camera_from_container(node) self._match_maya_render_mask(camera) - set_camera_resolution(camera, asset_doc=context["asset"]) + set_camera_resolution(camera, folder_entity=context["folder"]) self[:] = nodes return pipeline.containerise(node_name, @@ -133,16 +133,16 @@ class CameraLoader(load.LoaderPlugin): suffix="") def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") # Update attributes node.setParms({"fileName": file_path, - "representation": str(repre_doc["_id"])}) + "representation": repre_entity["id"]}) # Store the cam temporarily next to the Alembic Archive # so that we can preserve parm values the user set on it diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_fbx.py b/client/ayon_core/hosts/houdini/plugins/load/load_fbx.py index 4857dbb900..2ebbed5e12 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_fbx.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_fbx.py @@ -48,7 +48,7 @@ class FbxLoader(load.LoaderPlugin): return containerised_nodes def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] try: file_node = next( @@ -59,13 +59,13 @@ class FbxLoader(load.LoaderPlugin): return # Update the file path from representation - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") file_node.setParms({"file": file_path}) # Update attribute - node.setParms({"representation": str(repre_doc["_id"])}) + node.setParms({"representation": repre_entity["id"]}) def remove(self, container): @@ -79,7 +79,7 @@ class FbxLoader(load.LoaderPlugin): """Define node name.""" if not namespace: - namespace = context["asset"]["name"] + namespace = context["folder"]["name"] if namespace: node_name = "{}_{}".format(namespace, name) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py index ffe9e55036..07949fd177 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_hda.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_hda.py @@ -30,7 +30,7 @@ class HdaLoader(load.LoaderPlugin): # Create a unique name counter = 1 - namespace = namespace or context["asset"]["name"] + namespace = namespace or context["folder"]["name"] formatted = "{}_{}".format(namespace, name) if namespace else name node_name = "{0}_{1:03d}".format(formatted, counter) @@ -51,9 +51,9 @@ class HdaLoader(load.LoaderPlugin): def update(self, container, context): import hou - repre_doc = context["representation"] + repre_entity = context["representation"] hda_node = container["node"] - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") hou.hda.installFile(file_path) defs = hda_node.type().allInstalledDefinitions() @@ -61,7 +61,7 @@ class HdaLoader(load.LoaderPlugin): new = def_paths.index(file_path) defs[new].setIsPreferred(True) hda_node.setParms({ - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def remove(self, container): diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_image.py b/client/ayon_core/hosts/houdini/plugins/load/load_image.py index c89cc3b173..cc9dcc30c8 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_image.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_image.py @@ -64,7 +64,7 @@ class ImageLoader(load.LoaderPlugin): parent = get_image_avalon_container() # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name node = parent.createNode("file", node_name=node_name) @@ -79,7 +79,7 @@ class ImageLoader(load.LoaderPlugin): "name": node_name, "namespace": namespace, "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } # todo: add folder="Avalon" @@ -88,11 +88,11 @@ class ImageLoader(load.LoaderPlugin): return node def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") file_path = self._get_file_sequence(file_path) @@ -100,7 +100,7 @@ class ImageLoader(load.LoaderPlugin): node.setParms( { "filename1": file_path, - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], } ) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_redshift_proxy.py b/client/ayon_core/hosts/houdini/plugins/load/load_redshift_proxy.py index 3e9ce1ff2e..86c7ae0272 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_redshift_proxy.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_redshift_proxy.py @@ -26,7 +26,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node @@ -73,18 +73,18 @@ class RedshiftProxyLoader(load.LoaderPlugin): ) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) node = container["node"] node.setParms({ "RS_objprop_proxy_file": self.format_path( - file_path, repre_doc) + file_path, repre_entity) }) # Update attribute - node.setParms({"representation": str(repre_doc["_id"])}) + node.setParms({"representation": repre_entity["id"]}) def remove(self, container): diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_usd_layer.py b/client/ayon_core/hosts/houdini/plugins/load/load_usd_layer.py index f4f8a718ad..6ee21f87ec 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_usd_layer.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_usd_layer.py @@ -34,7 +34,7 @@ class USDSublayerLoader(load.LoaderPlugin): stage = hou.node("/stage") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create USD reference @@ -49,7 +49,7 @@ class USDSublayerLoader(load.LoaderPlugin): "name": node_name, "namespace": namespace, "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } # todo: add folder="Avalon" @@ -58,18 +58,18 @@ class USDSublayerLoader(load.LoaderPlugin): return container def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") # Update attributes node.setParms( { "filepath1": file_path, - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], } ) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_usd_reference.py b/client/ayon_core/hosts/houdini/plugins/load/load_usd_reference.py index cb83a9a22e..d0421083c6 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_usd_reference.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_usd_reference.py @@ -34,7 +34,7 @@ class USDReferenceLoader(load.LoaderPlugin): stage = hou.node("/stage") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create USD reference @@ -49,7 +49,7 @@ class USDReferenceLoader(load.LoaderPlugin): "name": node_name, "namespace": namespace, "loader": str(self.__class__.__name__), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], } # todo: add folder="Avalon" @@ -58,18 +58,18 @@ class USDReferenceLoader(load.LoaderPlugin): return container def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] # Update the file path - file_path = get_representation_path(repre_doc) + file_path = get_representation_path(repre_entity) file_path = file_path.replace("\\", "/") # Update attributes node.setParms( { "filepath1": file_path, - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], } ) diff --git a/client/ayon_core/hosts/houdini/plugins/load/load_vdb.py b/client/ayon_core/hosts/houdini/plugins/load/load_vdb.py index ed38e5a5d9..7b2803ab5d 100644 --- a/client/ayon_core/hosts/houdini/plugins/load/load_vdb.py +++ b/client/ayon_core/hosts/houdini/plugins/load/load_vdb.py @@ -26,7 +26,7 @@ class VdbLoader(load.LoaderPlugin): obj = hou.node("/obj") # Define node name - namespace = namespace if namespace else context["asset"]["name"] + namespace = namespace if namespace else context["folder"]["name"] node_name = "{}_{}".format(namespace, name) if namespace else name # Create a new geo node @@ -80,7 +80,7 @@ class VdbLoader(load.LoaderPlugin): return filename def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] node = container["node"] try: file_node = next( @@ -91,13 +91,13 @@ class VdbLoader(load.LoaderPlugin): return # Update the file path - file_path = get_representation_path(repre_doc) - file_path = self.format_path(file_path, repre_doc) + file_path = get_representation_path(repre_entity) + file_path = self.format_path(file_path, repre_entity) file_node.setParms({"file": file_path}) # Update attribute - node.setParms({"representation": str(repre_doc["_id"])}) + node.setParms({"representation": repre_entity["id"]}) def remove(self, container): diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_asset_handles.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_asset_handles.py index 6b62ea09d4..943a29952e 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_asset_handles.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_asset_handles.py @@ -8,7 +8,7 @@ from ayon_core.pipeline import AYONPyblishPluginMixin class CollectAssetHandles(pyblish.api.InstancePlugin, AYONPyblishPluginMixin): - """Apply asset handles. + """Apply folder handles. If instance does not have: - frameStart @@ -19,7 +19,7 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, - frameStartHandle - frameEndHandle - Then we will retrieve the asset's handles to compute + Then we will retrieve the folder's handles to compute the exclusive frame range and actual handle ranges. """ @@ -29,7 +29,7 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, # this plugin runs after CollectAnatomyInstanceData order = pyblish.api.CollectorOrder + 0.499 - label = "Collect Asset Handles" + label = "Collect Folder Handles" use_asset_handles = True def process(self, instance): @@ -52,9 +52,9 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, attr_values = self.get_attr_values_from_data(instance.data) if attr_values.get("use_handles", self.use_asset_handles): - asset_data = instance.data["assetEntity"]["data"] - handle_start = asset_data.get("handleStart", 0) - handle_end = asset_data.get("handleEnd", 0) + folder_attributes = instance.data["folderEntity"]["attrib"] + handle_start = folder_attributes.get("handleStart", 0) + handle_end = folder_attributes.get("handleEnd", 0) else: handle_start = 0 handle_end = 0 @@ -118,7 +118,7 @@ class CollectAssetHandles(pyblish.api.InstancePlugin, BoolDef("use_handles", tooltip="Disable this if you want the publisher to" " ignore start and end handles specified in the" - " asset data for this publish instance", + " folder attributes for this publish instance", default=cls.use_asset_handles, label="Use asset handles") ] diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py index edfa78e4d9..63537811cd 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances.py @@ -9,7 +9,7 @@ from ayon_core.hosts.houdini.api import lib class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by all node in out graph and pre-defined attributes - This collector takes into account assets that are associated with + This collector takes into account folders that are associated with an specific node and marked with a unique identifier; Identifier: diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_instances_usd_layered.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances_usd_layered.py index 38d6ec733d..9377a9fcd0 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_instances_usd_layered.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_instances_usd_layered.py @@ -15,10 +15,10 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): As opposed to storing `ayon.create.instance` as id on the node we store `pyblish.avalon.usdlayered`. - Additionally this instance has no need for storing family, asset, product - or name on the nodes. Instead all information is retrieved solely from - the output filepath, which is an Avalon URI: - avalon://{asset}/{product}.{representation} + Additionally this instance has no need for storing folder, product type, + product name or name on the nodes. Instead all information is retrieved + solely from the output filepath, which is an Avalon URI: + avalon://{folder}/{product}.{representation} Each final ROP node is considered a dependency for any of the Configured Save Path layers it sets along the way. As such, the instances shown in @@ -89,7 +89,7 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): # For now group ALL of them into USD Layer product group # Allow this product to be grouped into a USD Layer on creation - data["subsetGroup"] = "USD Layer" + data["productGroup"] = "USD Layer" instances = list() dependencies = [] @@ -142,9 +142,9 @@ class CollectInstancesUsdLayered(pyblish.api.ContextPlugin): self.log.warning("Non Avalon URI Layer Path: %s" % save_path) return {} - # Collect asset + product from URI - name = "{product[name]} ({asset})".format(**uri_data) - fname = "{asset}_{product[name]}.{ext}".format(**uri_data) + # Collect folder + product from URI + name = "{product[name]} ({folder[path]})".format(**uri_data) + fname = "{folder[path]}_{product[name]}.{ext}".format(**uri_data) data = dict(uri_data) data["usdSavePath"] = save_path diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_bootstrap.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_bootstrap.py index 24ac9f22c3..cd82f1679a 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_bootstrap.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_bootstrap.py @@ -1,11 +1,7 @@ import pyblish.api +import ayon_api -from ayon_core.client import ( - get_subset_by_name, - get_asset_by_name, - get_asset_name_identifier, -) -from ayon_core.pipeline import usdlib +from ayon_core.pipeline import usdlib, KnownPublishError class CollectUsdBootstrap(pyblish.api.InstancePlugin): @@ -54,9 +50,13 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): self.log.debug("Add bootstrap for: %s" % bootstrap) project_name = instance.context.data["projectName"] - asset_name = instance.data["folderPath"] - asset_doc = get_asset_by_name(project_name, asset_name) - assert asset_doc, "Asset must exist: %s" % asset_name + folder_path = instance.data["folderPath"] + folder_name = folder_path.rsplit("/", 1)[-1] + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + if not folder_entity: + raise KnownPublishError( + "Folder '{}' does not exist".format(folder_path) + ) # Check which are not about to be created and don't exist yet required = {"shot": ["usdShot"], "asset": ["usdAsset"]}.get(bootstrap) @@ -73,20 +73,20 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): self.log.debug("Checking required bootstrap: %s" % required) for product_name in required: if self._product_exists( - project_name, instance, product_name, asset_doc + project_name, instance, product_name, folder_entity ): continue self.log.debug( "Creating {0} USD bootstrap: {1} {2}".format( - bootstrap, asset_name, product_name + bootstrap, folder_path, product_name ) ) product_type = "usd.bootstrap" new = instance.context.create_instance(product_name) new.data["productName"] = product_name - new.data["label"] = "{0} ({1})".format(product_name, asset_name) + new.data["label"] = "{0} ({1})".format(product_name, folder_name) new.data["productType"] = product_type new.data["family"] = product_type new.data["comment"] = "Automated bootstrap USD file." @@ -100,24 +100,24 @@ class CollectUsdBootstrap(pyblish.api.InstancePlugin): new.data[key] = instance.data[key] def _product_exists( - self, project_name, instance, product_name, asset_doc + self, project_name, instance, product_name, folder_entity ): """Return whether product exists in current context or in database.""" # Allow it to be created during this publish session context = instance.context - asset_doc_name = get_asset_name_identifier(asset_doc) + folder_path = folder_entity["path"] for inst in context: if ( inst.data["productName"] == product_name - and inst.data["folderPath"] == asset_doc_name + and inst.data["folderPath"] == folder_path ): return True # Or, if they already exist in the database we can # skip them too. - if get_subset_by_name( - project_name, product_name, asset_doc["_id"], fields=["_id"] + if ayon_api.get_product_by_name( + project_name, product_name, folder_entity["id"], fields={"id"} ): return True return False diff --git a/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_layers.py b/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_layers.py index f085b6ca41..93add6806e 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_layers.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/collect_usd_layers.py @@ -64,4 +64,4 @@ class CollectUsdLayers(pyblish.api.InstancePlugin): layer_inst.append((layer, save_path)) # Allow this product to be grouped into a USD Layer on creation - layer_inst.data["subsetGroup"] = "USD Layer" + layer_inst.data["productGroup"] = "USD Layer" diff --git a/client/ayon_core/hosts/houdini/plugins/publish/extract_usd_layered.py b/client/ayon_core/hosts/houdini/plugins/publish/extract_usd_layered.py index 99c61803e6..2e5c9a892c 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/extract_usd_layered.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/extract_usd_layered.py @@ -1,17 +1,12 @@ import os import contextlib -import hou import sys from collections import deque +import hou +import ayon_api import pyblish.api -from ayon_core.client import ( - get_asset_by_name, - get_subset_by_name, - get_last_version_by_subset_id, - get_representation_by_name, -) from ayon_core.pipeline import ( get_representation_path, publish, @@ -284,29 +279,29 @@ class ExtractUSDLayered(publish.Extractor): # Compare this dependency with the latest published version # to detect whether we should make this into a new publish # version. If not, skip it. - asset_doc = get_asset_by_name( - project_name, dependency.data["folderPath"], fields=["_id"] + folder_entity = ayon_api.get_folder_by_path( + project_name, dependency.data["folderPath"], fields={"id"} ) - subset_doc = get_subset_by_name( + product_entity = ayon_api.get_product_by_name( project_name, dependency.data["productName"], - asset_doc["_id"], - fields=["_id"] + folder_entity["id"], + fields={"id"} ) - if not subset_doc: + if not product_entity: # Subset doesn't exist yet. Definitely new file self.log.debug("No existing product..") return False - version_doc = get_last_version_by_subset_id( - project_name, subset_doc["_id"], fields=["_id"] + version_entity = ayon_api.get_last_version_by_product_id( + project_name, product_entity["id"], fields={"id"} ) - if not version_doc: + if not version_entity: self.log.debug("No existing version..") return False - representation = get_representation_by_name( - project_name, ext.lstrip("."), version_doc["_id"] + representation = ayon_api.get_representation_by_name( + project_name, ext.lstrip("."), version_entity["id"] ) if not representation: self.log.debug("No existing representation..") diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_frame_range.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_frame_range.py index 36e1b9b2a5..2a3418ee7e 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_frame_range.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_frame_range.py @@ -7,8 +7,8 @@ from ayon_core.hosts.houdini.api.action import SelectInvalidAction import hou -class DisableUseAssetHandlesAction(RepairAction): - label = "Disable use asset handles" +class DisableUseFolderHandlesAction(RepairAction): + label = "Disable use folder handles" icon = "mdi.toggle-switch-off" @@ -23,7 +23,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): order = pyblish.api.ValidatorOrder - 0.1 hosts = ["houdini"] label = "Validate Frame Range" - actions = [DisableUseAssetHandlesAction, SelectInvalidAction] + actions = [DisableUseFolderHandlesAction, SelectInvalidAction] def process(self, instance): @@ -41,11 +41,11 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): "## Invalid Frame Range\n" "The frame range for the instance is invalid because " "the start frame is higher than the end frame.\n\nThis " - "is likely due to asset handles being applied to your " + "is likely due to folder handles being applied to your " "instance or the ROP node's start frame " "is set higher than the end frame.\n\nIf your ROP frame " - "range is correct and you do not want to apply asset " - "handles make sure to disable Use asset handles on the " + "range is correct and you do not want to apply folder " + "handles make sure to disable Use folder handles on the " "publish instance." ) ) @@ -71,7 +71,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): cls.log.info( "The ROP node render range is set to " "{0[frameStartHandle]} - {0[frameEndHandle]} " - "The asset handles applied to the instance are start handle " + "The folder handles applied to the instance are start handle " "{0[handleStart]} and end handle {0[handleEnd]}" .format(instance.data) ) @@ -84,7 +84,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): # Already fixed return - # Disable use asset handles + # Disable use folder handles context = instance.context create_context = context.data["create_context"] instance_id = instance.data.get("instance_id") @@ -102,5 +102,5 @@ class ValidateFrameRange(pyblish.api.InstancePlugin): created_instance.publish_attributes["CollectAssetHandles"]["use_handles"] = False # noqa create_context.save_changes() - cls.log.debug("use asset handles is turned off for '{}'" + cls.log.debug("use folder handles is turned off for '{}'" .format(instance)) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py index e94f09568d..0481929824 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_subset_name.py @@ -15,21 +15,21 @@ from ayon_core.pipeline.create import get_product_name import hou -class FixSubsetNameAction(RepairAction): - label = "Fix Subset Name" +class FixProductNameAction(RepairAction): + label = "Fix Product Name" class ValidateSubsetName(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Validate Subset name. + """Validate Product name. """ families = ["staticMesh"] hosts = ["houdini"] - label = "Validate Subset Name" + label = "Validate Product Name" order = ValidateContentsOrder + 0.1 - actions = [FixSubsetNameAction, SelectInvalidAction] + actions = [FixProductNameAction, SelectInvalidAction] optional = True @@ -54,15 +54,20 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) # Check product name - asset_doc = instance.data["assetEntity"] + folder_entity = instance.data["folderEntity"] + task_entity = instance.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] product_name = get_product_name( instance.context.data["projectName"], - asset_doc, - instance.data["task"], + task_name, + task_type, instance.context.data["hostName"], instance.data["productType"], variant=instance.data["variant"], - dynamic_data={"asset": asset_doc["name"]} + dynamic_data={"asset": folder_entity["name"]} ) if instance.data.get("productName") != product_name: @@ -79,15 +84,20 @@ class ValidateSubsetName(pyblish.api.InstancePlugin, rop_node = hou.node(instance.data["instance_node"]) # Check product name - asset_doc = instance.data["assetEntity"] + folder_entity = instance.data["folderEntity"] + task_entity = instance.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] product_name = get_product_name( instance.context.data["projectName"], - asset_doc, - instance.data["task"], + task_name, + task_type, instance.context.data["hostName"], instance.data["productType"], variant=instance.data["variant"], - dynamic_data={"asset": asset_doc["name"]} + dynamic_data={"asset": folder_entity["name"]} ) instance.data["productName"] = product_name diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py index 33d0d42383..ae00bc9db4 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_unreal_staticmesh_naming.py @@ -24,7 +24,7 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, - UCX This validator also checks if product name is correct - - {static mesh prefix}_{Asset-Name}{Variant}. + - {static mesh prefix}_{FolderName}{Variant}. """ diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py index 6d21b59a9c..048d675c00 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_model_exists.py @@ -1,11 +1,14 @@ # -*- coding: utf-8 -*- import re +import ayon_api import pyblish.api -from ayon_core.client import get_subset_by_name -from ayon_core.pipeline.publish import ValidateContentsOrder -from ayon_core.pipeline import PublishValidationError +from ayon_core.pipeline.publish import ( + ValidateContentsOrder, + KnownPublishError, + PublishValidationError, +) class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin): @@ -18,7 +21,7 @@ class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin): def process(self, instance): project_name = instance.context.data["projectName"] - asset_name = instance.data["folderPath"] + folder_path = instance.data["folderPath"] product_name = instance.data["productName"] # Assume shading variation starts after a dot separator @@ -27,16 +30,21 @@ class ValidateUSDShadeModelExists(pyblish.api.InstancePlugin): "^usdShade", "usdModel", shade_product_name ) - asset_doc = instance.data.get("assetEntity") - if not asset_doc: - raise RuntimeError("Asset document is not filled on instance.") + folder_entity = instance.data.get("folderEntity") + if not folder_entity: + raise KnownPublishError( + "Folder entity is not filled on instance." + ) - subset_doc = get_subset_by_name( - project_name, model_product_name, asset_doc["_id"], fields=["_id"] + product_entity = ayon_api.get_product_by_name( + project_name, + model_product_name, + folder_entity["id"], + fields={"id"} ) - if not subset_doc: + if not product_entity: raise PublishValidationError( ("USD Model product not found: " - "{} ({})").format(model_product_name, asset_name), + "{} ({})").format(model_product_name, folder_path), title=self.label ) diff --git a/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_workspace.py b/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_workspace.py index d85f20e3ce..2ea4b5d816 100644 --- a/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_workspace.py +++ b/client/ayon_core/hosts/houdini/plugins/publish/validate_usd_shade_workspace.py @@ -53,7 +53,7 @@ class ValidateUsdShadeWorkspace(pyblish.api.InstancePlugin): # There were some issues with the editable node not having the right # configured path. So for now let's assure that is correct to.from value = ( - 'avalon://`chs("../asset_name")`/' + 'avalon://`chs("../folder_path")`/' 'usdShade`chs("../model_variantname1")`.usd' ) rop_value = rop.parm("lopoutput").rawValue() diff --git a/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml b/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml index b2ea142cd5..b93445a974 100644 --- a/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml +++ b/client/ayon_core/hosts/houdini/startup/MainMenuCommon.xml @@ -6,10 +6,10 @@ import os return os.environ.get("AYON_MENU_LABEL") or "AYON" ]]> - + diff --git a/client/ayon_core/hosts/max/api/lib.py b/client/ayon_core/hosts/max/api/lib.py index 05c3364e4a..5f13856c9b 100644 --- a/client/ayon_core/hosts/max/api/lib.py +++ b/client/ayon_core/hosts/max/api/lib.py @@ -6,10 +6,13 @@ import json from typing import Any, Dict, Union import six +import ayon_api + from ayon_core.pipeline import get_current_project_name, colorspace from ayon_core.settings import get_project_settings from ayon_core.pipeline.context_tools import ( - get_current_project, get_current_project_asset) + get_current_project_folder, +) from ayon_core.style import load_stylesheet from pymxs import runtime as rt @@ -215,49 +218,44 @@ def set_scene_resolution(width: int, height: int): def reset_scene_resolution(): """Apply the scene resolution from the project definition - scene resolution can be overwritten by an asset if the asset.data contains - any information regarding scene resolution . - Returns: - None + scene resolution can be overwritten by a folder if the folder.attrib + contains any information regarding scene resolution. """ - data = ["data.resolutionWidth", "data.resolutionHeight"] - project_resolution = get_current_project(fields=data) - project_resolution_data = project_resolution["data"] - asset_resolution = get_current_project_asset(fields=data) - asset_resolution_data = asset_resolution["data"] - # Set project resolution - project_width = int(project_resolution_data.get("resolutionWidth", 1920)) - project_height = int(project_resolution_data.get("resolutionHeight", 1080)) - width = int(asset_resolution_data.get("resolutionWidth", project_width)) - height = int(asset_resolution_data.get("resolutionHeight", project_height)) + + folder_entity = get_current_project_folder( + fields={"attrib.resolutionWidth", "attrib.resolutionHeight"} + ) + folder_attributes = folder_entity["attrib"] + width = int(folder_attributes["resolutionWidth"]) + height = int(folder_attributes["resolutionHeight"]) set_scene_resolution(width, height) -def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: - """Get the current assets frame range and handles. +def get_frame_range(folder_entiy=None) -> Union[Dict[str, Any], None]: + """Get the current folder frame range and handles. Args: - asset_doc (dict): Asset Entity Data + folder_entiy (dict): Folder eneity. Returns: dict: with frame start, frame end, handle start, handle end. """ # Set frame start/end - if asset_doc is None: - asset_doc = get_current_project_asset() + if folder_entiy is None: + folder_entiy = get_current_project_folder() - data = asset_doc["data"] - frame_start = data.get("frameStart") - frame_end = data.get("frameEnd") + folder_attributes = folder_entiy["attrib"] + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") if frame_start is None or frame_end is None: return {} frame_start = int(frame_start) frame_end = int(frame_end) - handle_start = int(data.get("handleStart", 0)) - handle_end = int(data.get("handleEnd", 0)) + handle_start = int(folder_attributes.get("handleStart", 0)) + handle_end = int(folder_attributes.get("handleEnd", 0)) frame_start_handle = frame_start - handle_start frame_end_handle = frame_end + handle_end @@ -272,7 +270,7 @@ def get_frame_range(asset_doc=None) -> Union[Dict[str, Any], None]: def reset_frame_range(fps: bool = True): - """Set frame range to current asset. + """Set frame range to current folder. This is part of 3dsmax documentation: animationRange: A System Global variable which lets you get and @@ -283,8 +281,9 @@ def reset_frame_range(fps: bool = True): scene frame rate in frames-per-second. """ if fps: - data_fps = get_current_project(fields=["data.fps"]) - fps_number = float(data_fps["data"]["fps"]) + project_name = get_current_project_name() + project_entity = ayon_api.get_project(project_name) + fps_number = float(project_entity["attrib"].get("fps")) rt.frameRate = fps_number frame_range = get_frame_range() @@ -328,7 +327,7 @@ def convert_unit_scale(): def set_context_setting(): """Apply the project settings from the project definition - Settings can be overwritten by an asset if the asset.data contains + Settings can be overwritten by an folder if the folder.attrib contains any information regarding those settings. Examples of settings: diff --git a/client/ayon_core/hosts/max/api/lib_rendersettings.py b/client/ayon_core/hosts/max/api/lib_rendersettings.py index 7ffc024ba3..8a9881f032 100644 --- a/client/ayon_core/hosts/max/api/lib_rendersettings.py +++ b/client/ayon_core/hosts/max/api/lib_rendersettings.py @@ -3,7 +3,7 @@ from pymxs import runtime as rt from ayon_core.lib import Logger from ayon_core.settings import get_project_settings from ayon_core.pipeline import get_current_project_name -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.hosts.max.api.lib import ( set_render_frame_range, @@ -57,14 +57,14 @@ class RenderSettings(object): if not os.path.exists(output_dir): os.makedirs(output_dir) # hard-coded, should be customized in the setting - context = get_current_project_asset() + folder_attributes = get_current_project_folder()["attrib"] # get project resolution - width = context["data"].get("resolutionWidth") - height = context["data"].get("resolutionHeight") + width = folder_attributes.get("resolutionWidth") + height = folder_attributes.get("resolutionHeight") # Set Frame Range - frame_start = context["data"].get("frame_start") - frame_end = context["data"].get("frame_end") + frame_start = folder_attributes.get("frame_start") + frame_end = folder_attributes.get("frame_end") set_render_frame_range(frame_start, frame_end) # get the production render renderer_class = get_current_renderer() diff --git a/client/ayon_core/hosts/max/api/pipeline.py b/client/ayon_core/hosts/max/api/pipeline.py index 1486f7218d..6fd0a501ff 100644 --- a/client/ayon_core/hosts/max/api/pipeline.py +++ b/client/ayon_core/hosts/max/api/pipeline.py @@ -169,7 +169,7 @@ def containerise(name: str, nodes: list, context, "name": name, "namespace": namespace or "", "loader": loader, - "representation": context["representation"]["_id"], + "representation": context["representation"]["id"], } container_name = f"{namespace}:{name}{suffix}" container = rt.container(name=container_name) diff --git a/client/ayon_core/hosts/max/plugins/create/create_workfile.py b/client/ayon_core/hosts/max/plugins/create/create_workfile.py index 058fe10eb2..901da6254c 100644 --- a/client/ayon_core/hosts/max/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/max/plugins/create/create_workfile.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +import ayon_api + from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.client import get_asset_by_name, get_asset_name_identifier from ayon_core.hosts.max.api import plugin from ayon_core.hosts.max.api.lib import read, imprint from pymxs import runtime as rt @@ -24,21 +25,26 @@ class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator): if instance.creator_identifier == self.identifier ), None) project_name = self.project_name - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name if current_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": variant } @@ -46,8 +52,8 @@ class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator): data.update( self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, current_instance) @@ -61,21 +67,25 @@ class CreateWorkfile(plugin.MaxCreatorBase, AutoCreator): self._add_instance_to_context(current_instance) imprint(instance_node.name, current_instance.data) elif ( - current_instance["folderPath"] != asset_name + current_instance["folderPath"] != folder_path or current_instance["task"] != task_name ): # Update instance context if is not the same - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) - asset_name = get_asset_name_identifier(asset_doc) - current_instance["folderPath"] = asset_name + current_instance["folderPath"] = folder_entity["path"] current_instance["task"] = task_name current_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py b/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py index d56445c695..664904eb4e 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py +++ b/client/ayon_core/hosts/max/plugins/load/load_camera_fbx.py @@ -54,8 +54,8 @@ class FbxLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, _ = get_namespace(node_name) @@ -88,7 +88,7 @@ class FbxLoader(load.LoaderPlugin): update_custom_attribute_data(node, fbx_objects) lib.imprint(container["instance_node"], { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_max_scene.py b/client/ayon_core/hosts/max/plugins/load/load_max_scene.py index 9707297c2f..0090b2256a 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_max_scene.py +++ b/client/ayon_core/hosts/max/plugins/load/load_max_scene.py @@ -123,8 +123,8 @@ class MaxSceneLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, _ = get_namespace(node_name) @@ -164,7 +164,7 @@ class MaxSceneLoader(load.LoaderPlugin): update_custom_attribute_data(node, max_objects) lib.imprint(container["instance_node"], { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_model.py b/client/ayon_core/hosts/max/plugins/load/load_model.py index e0241bdb73..0c39c1ba0d 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model.py @@ -73,8 +73,8 @@ class ModelAbcLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node = rt.GetNodeByName(container["instance_node"]) node_list = [n for n in get_previous_loaded_object(node) if rt.ClassOf(n) == rt.AlembicContainer] @@ -91,7 +91,7 @@ class ModelAbcLoader(load.LoaderPlugin): abc_obj.source = path lib.imprint( container["instance_node"], - {"representation": str(repre_doc["_id"])}, + {"representation": repre_entity["id"]}, ) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py b/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py index 03ba901b32..7f5f1255ec 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_fbx.py @@ -50,8 +50,8 @@ class FbxModelLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node_name = container["instance_node"] node = rt.getNodeByName(node_name) if not node: @@ -86,7 +86,7 @@ class FbxModelLoader(load.LoaderPlugin): rt.Select(node) update_custom_attribute_data(node, fbx_objects) lib.imprint(container["instance_node"], { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py b/client/ayon_core/hosts/max/plugins/load/load_model_obj.py index a6c3d2a2fe..4b8d260921 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_obj.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_obj.py @@ -50,8 +50,8 @@ class ObjLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node_name = container["instance_node"] node = rt.getNodeByName(node_name) namespace, _ = get_namespace(node_name) @@ -78,7 +78,7 @@ class ObjLoader(load.LoaderPlugin): rt.Select(node) lib.imprint(node_name, { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_model_usd.py b/client/ayon_core/hosts/max/plugins/load/load_model_usd.py index 6673a2e48b..6bcff2b6a5 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_model_usd.py +++ b/client/ayon_core/hosts/max/plugins/load/load_model_usd.py @@ -66,8 +66,8 @@ class ModelUSDLoader(load.LoaderPlugin): namespace, loader=self.__class__.__name__) def update(self, container, context): - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node_name = container["instance_node"] node = rt.GetNodeByName(node_name) namespace, name = get_namespace(node_name) @@ -108,7 +108,7 @@ class ModelUSDLoader(load.LoaderPlugin): rt.Select(node) lib.imprint(node_name, { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcache.py b/client/ayon_core/hosts/max/plugins/load/load_pointcache.py index 6f79caea42..613d240447 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_pointcache.py +++ b/client/ayon_core/hosts/max/plugins/load/load_pointcache.py @@ -79,8 +79,8 @@ class AbcLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node = rt.GetNodeByName(container["instance_node"]) abc_container = [n for n in get_previous_loaded_object(node) if rt.ClassOf(n) == rt.AlembicContainer] @@ -97,7 +97,7 @@ class AbcLoader(load.LoaderPlugin): abc_obj.source = path lib.imprint( container["instance_node"], - {"representation": str(repre_doc["_id"])}, + {"representation": repre_entity["id"]}, ) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py b/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py index 67d1374266..7cf7d162c1 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py +++ b/client/ayon_core/hosts/max/plugins/load/load_pointcache_ornatrix.py @@ -63,8 +63,8 @@ class OxAbcLoader(load.LoaderPlugin): ) def update(self, container, context): - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node_name = container["instance_node"] namespace, name = get_namespace(node_name) node = rt.getNodeByName(node_name) @@ -99,7 +99,7 @@ class OxAbcLoader(load.LoaderPlugin): update_custom_attribute_data(node, ox_abc_objects) lib.imprint( container["instance_node"], - {"representation": str(repre_doc["_id"])}, + {"representation": repre_entity["id"]}, ) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py b/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py index 894648ff23..6b9e5d6fb1 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py +++ b/client/ayon_core/hosts/max/plugins/load/load_pointcloud.py @@ -45,8 +45,8 @@ class PointCloudLoader(load.LoaderPlugin): """update the container""" from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) update_custom_attribute_data( @@ -56,7 +56,7 @@ class PointCloudLoader(load.LoaderPlugin): for prt in rt.Selection: prt.filename = path lib.imprint(container["instance_node"], { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py b/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py index 7395a6eca5..40c7701797 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py +++ b/client/ayon_core/hosts/max/plugins/load/load_redshift_proxy.py @@ -55,8 +55,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): def update(self, container, context): from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node = rt.getNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) rt.Select(node_list) @@ -66,7 +66,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): proxy.file = path lib.imprint(container["instance_node"], { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/load/load_tycache.py b/client/ayon_core/hosts/max/plugins/load/load_tycache.py index 5acc759b4a..24cb762b2d 100644 --- a/client/ayon_core/hosts/max/plugins/load/load_tycache.py +++ b/client/ayon_core/hosts/max/plugins/load/load_tycache.py @@ -43,8 +43,8 @@ class TyCacheLoader(load.LoaderPlugin): """update the container""" from pymxs import runtime as rt - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) node = rt.GetNodeByName(container["instance_node"]) node_list = get_previous_loaded_object(node) update_custom_attribute_data(node, node_list) @@ -52,7 +52,7 @@ class TyCacheLoader(load.LoaderPlugin): for tyc in node_list: tyc.filename = path lib.imprint(container["instance_node"], { - "representation": str(repre_doc["_id"]) + "representation": repre_entity["id"] }) def switch(self, container, context): diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_deadline_publish.py b/client/ayon_core/hosts/max/plugins/publish/validate_deadline_publish.py deleted file mode 100644 index 2c9ca4ae64..0000000000 --- a/client/ayon_core/hosts/max/plugins/publish/validate_deadline_publish.py +++ /dev/null @@ -1,43 +0,0 @@ -import os -import pyblish.api -from pymxs import runtime as rt -from ayon_core.pipeline.publish import ( - RepairAction, - ValidateContentsOrder, - PublishValidationError, - OptionalPyblishPluginMixin -) -from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings - - -class ValidateDeadlinePublish(pyblish.api.InstancePlugin, - OptionalPyblishPluginMixin): - """Validates Render File Directory is - not the same in every submission - """ - - order = ValidateContentsOrder - families = ["maxrender"] - hosts = ["max"] - label = "Render Output for Deadline" - optional = True - actions = [RepairAction] - - def process(self, instance): - if not self.is_active(instance.data): - return - file = rt.maxFileName - filename, ext = os.path.splitext(file) - if filename not in rt.rendOutputFilename: - raise PublishValidationError( - "Render output folder " - "doesn't match the max scene name! " - "Use Repair action to " - "fix the folder file path.." - ) - - @classmethod - def repair(cls, instance): - container = instance.data.get("instance_node") - RenderSettings().render_output(container) - cls.log.debug("Reset the render output folder...") diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_frame_range.py b/client/ayon_core/hosts/max/plugins/publish/validate_frame_range.py index 22fda37e61..2f4ec5f86c 100644 --- a/client/ayon_core/hosts/max/plugins/publish/validate_frame_range.py +++ b/client/ayon_core/hosts/max/plugins/publish/validate_frame_range.py @@ -18,11 +18,11 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, """Validates the frame ranges. This is an optional validator checking if the frame range on instance - matches the frame range specified for the asset. + matches the frame range specified for the folder. It also validates render frame ranges of render layers. - Repair action will change everything to match the asset frame range. + Repair action will change everything to match the folder frame range. This can be turned off by the artist to allow custom ranges. """ @@ -42,7 +42,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, return frame_range = get_frame_range( - asset_doc=instance.data["assetEntity"]) + instance.data["folderEntity"]) inst_frame_start = instance.data.get("frameStartHandle") inst_frame_end = instance.data.get("frameEndHandle") @@ -57,12 +57,12 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if frame_start_handle != inst_frame_start: errors.append( f"Start frame ({inst_frame_start}) on instance does not match " # noqa - f"with the start frame ({frame_start_handle}) set on the asset data. ") # noqa + f"with the start frame ({frame_start_handle}) set on the folder attributes. ") # noqa if frame_end_handle != inst_frame_end: errors.append( f"End frame ({inst_frame_end}) on instance does not match " f"with the end frame ({frame_end_handle}) " - "from the asset data. ") + "from the folder attributes. ") if errors: bullet_point_errors = "\n".join( diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py b/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py new file mode 100644 index 0000000000..ba948747b9 --- /dev/null +++ b/client/ayon_core/hosts/max/plugins/publish/validate_renderpasses.py @@ -0,0 +1,185 @@ +import os +import pyblish.api +from pymxs import runtime as rt +from ayon_core.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from ayon_core.hosts.max.api.lib_rendersettings import RenderSettings + + +class ValidateRenderPasses(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): + """Validates Render Passes before farm submission + """ + + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + label = "Validate Render Passes" + actions = [RepairAction] + + def process(self, instance): + invalid = self.get_invalid(instance) + if invalid: + bullet_point_invalid_statement = "\n".join( + f"- {err_type}: {filepath}" for err_type, filepath + in invalid + ) + report = ( + "Invalid render passes found.\n\n" + f"{bullet_point_invalid_statement}\n\n" + "You can use repair action to fix the invalid filepath." + ) + raise PublishValidationError( + report, title="Invalid Render Passes") + + @classmethod + def get_invalid(cls, instance): + """Function to get invalid beauty render outputs and + render elements. + + 1. Check Render Output Folder matches the name of + the current Max Scene, e.g. + The name of the current Max scene: + John_Doe.max + The expected render output directory: + {root[work]}/{project[name]}/{hierarchy}/{asset}/ + work/{task[name]}/render/3dsmax/John_Doe/ + + 2. Check image extension(s) of the render output(s) + matches the image format in OP/AYON setting, e.g. + The current image format in settings: png + The expected render outputs: John_Doe.png + + 3. Check filename of render element ends with the name of + render element from the 3dsMax Render Element Manager. + e.g. The name of render element: RsCryptomatte + The expected filename: {InstanceName}_RsCryptomatte.png + + Args: + instance (pyblish.api.Instance): instance + workfile_name (str): filename of the Max scene + + Returns: + list: list of invalid filename which doesn't match + with the project name + """ + invalid = [] + file = rt.maxFileName + workfile_name, ext = os.path.splitext(file) + if workfile_name not in rt.rendOutputFilename: + cls.log.error( + "Render output folder must include" + f" the max scene name {workfile_name} " + ) + invalid_folder_name = os.path.dirname( + rt.rendOutputFilename).replace( + "\\", "/").split("/")[-1] + invalid.append(("Invalid Render Output Folder", + invalid_folder_name)) + beauty_fname = os.path.basename(rt.rendOutputFilename) + beauty_name, ext = os.path.splitext(beauty_fname) + invalid_filenames = cls.get_invalid_filenames( + instance, beauty_name) + invalid.extend(invalid_filenames) + invalid_image_format = cls.get_invalid_image_format( + instance, ext.lstrip(".")) + invalid.extend(invalid_image_format) + renderer = instance.data["renderer"] + if renderer in [ + "ART_Renderer", + "Redshift_Renderer", + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3", + "Default_Scanline_Renderer", + "Quicksilver_Hardware_Renderer", + ]: + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + renderpass = str(renderlayer_name).rsplit(":", 1)[-1] + rend_file = render_elem.GetRenderElementFilename(i) + if not rend_file: + continue + + rend_fname, ext = os.path.splitext( + os.path.basename(rend_file)) + invalid_filenames = cls.get_invalid_filenames( + instance, rend_fname, renderpass=renderpass) + invalid.extend(invalid_filenames) + invalid_image_format = cls.get_invalid_image_format( + instance, ext) + invalid.extend(invalid_image_format) + elif renderer == "Arnold": + cls.log.debug( + "Renderpass validation does not support Arnold yet," + " validation skipped...") + else: + cls.log.debug( + "Skipping render element validation " + f"for renderer: {renderer}") + return invalid + + @classmethod + def get_invalid_filenames(cls, instance, file_name, renderpass=None): + """Function to get invalid filenames from render outputs. + + Args: + instance (pyblish.api.Instance): instance + file_name (str): name of the file + renderpass (str, optional): name of the renderpass. + Defaults to None. + + Returns: + list: invalid filenames + """ + invalid = [] + if instance.name not in file_name: + cls.log.error("The renderpass filename should contain the instance name.") + invalid.append((f"Invalid instance name", + file_name)) + if renderpass is not None: + if not file_name.rstrip(".").endswith(renderpass): + cls.log.error( + f"Filename for {renderpass} should " + f"end with {renderpass}: {file_name}" + ) + invalid.append((f"Invalid {renderpass}", + os.path.basename(file_name))) + return invalid + + @classmethod + def get_invalid_image_format(cls, instance, ext): + """Function to check if the image format of the render outputs + aligns with that in the setting. + + Args: + instance (pyblish.api.Instance): instance + ext (str): image extension + + Returns: + list: list of files with invalid image format + """ + invalid = [] + settings = instance.context.data["project_settings"].get("max") + image_format = settings["RenderSettings"]["image_format"] + ext = ext.lstrip(".") + if ext != image_format: + msg = ( + f"Invalid image format {ext} for render outputs.\n" + f"Should be: {image_format}") + cls.log.error(msg) + invalid.append((msg, ext)) + return invalid + + @classmethod + def repair(cls, instance): + container = instance.data.get("instance_node") + # TODO: need to rename the function of render_output + RenderSettings().render_output(container) + cls.log.debug("Finished repairing the render output " + "folder and filenames.") diff --git a/client/ayon_core/hosts/max/plugins/publish/validate_resolution_setting.py b/client/ayon_core/hosts/max/plugins/publish/validate_resolution_setting.py index 0058d3b262..f499f851f1 100644 --- a/client/ayon_core/hosts/max/plugins/publish/validate_resolution_setting.py +++ b/client/ayon_core/hosts/max/plugins/publish/validate_resolution_setting.py @@ -24,7 +24,7 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, def process(self, instance): if not self.is_active(instance.data): return - width, height = self.get_db_resolution(instance) + width, height = self.get_folder_resolution(instance) current_width = rt.renderWidth current_height = rt.renderHeight if current_width != width and current_height != height: @@ -41,16 +41,15 @@ class ValidateResolutionSetting(pyblish.api.InstancePlugin, "not matching resolution set " "on asset or shot.") - def get_db_resolution(self, instance): - asset_doc = instance.data["assetEntity"] - project_doc = instance.context.data["projectEntity"] - for data in [asset_doc["data"], project_doc["data"]]: - if "resolutionWidth" in data and "resolutionHeight" in data: - width = data["resolutionWidth"] - height = data["resolutionHeight"] - return int(width), int(height) + def get_folder_resolution(self, instance): + folder_entity = instance.data["folderEntity"] + if folder_entity: + folder_attributes = folder_entity["attrib"] + width = folder_attributes["resolutionWidth"] + height = folder_attributes["resolutionHeight"] + return int(width), int(height) - # Defaults if not found in asset document or project document + # Defaults if not found in folder entity return 1920, 1080 @classmethod diff --git a/client/ayon_core/hosts/maya/api/action.py b/client/ayon_core/hosts/maya/api/action.py index 4beb1e3e5b..baf558036e 100644 --- a/client/ayon_core/hosts/maya/api/action.py +++ b/client/ayon_core/hosts/maya/api/action.py @@ -2,8 +2,8 @@ from __future__ import absolute_import import pyblish.api +import ayon_api -from ayon_core.client import get_asset_by_name from ayon_core.pipeline.publish import get_errored_instances_from_context @@ -74,21 +74,23 @@ class GenerateUUIDsOnInvalidAction(pyblish.api.Action): from . import lib - # Expecting this is called on validators in which case 'assetEntity' + # Expecting this is called on validators in which case 'folderEntity' # should be always available, but kept a way to query it by name. - asset_doc = instance.data.get("assetEntity") - if not asset_doc: - asset_name = instance.data["folderPath"] + folder_entity = instance.data.get("folderEntity") + if not folder_entity: + folder_path = instance.data["folderPath"] project_name = instance.context.data["projectName"] self.log.info(( - "Asset is not stored on instance." - " Querying by name \"{}\" from project \"{}\"" - ).format(asset_name, project_name)) - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["_id"] + "Folder is not stored on instance." + " Querying by path \"{}\" from project \"{}\"" + ).format(folder_path, project_name)) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} ) - for node, _id in lib.generate_ids(nodes, asset_id=asset_doc["_id"]): + for node, _id in lib.generate_ids( + nodes, folder_id=folder_entity["id"] + ): lib.set_id(node, _id, overwrite=True) diff --git a/client/ayon_core/hosts/maya/api/commands.py b/client/ayon_core/hosts/maya/api/commands.py index f69dca97a8..e63800e542 100644 --- a/client/ayon_core/hosts/maya/api/commands.py +++ b/client/ayon_core/hosts/maya/api/commands.py @@ -2,8 +2,9 @@ """OpenPype script commands to be used directly in Maya.""" from maya import cmds -from ayon_core.client import get_asset_by_name, get_project -from ayon_core.pipeline import get_current_project_name, get_current_asset_name +from ayon_api import get_project, get_folder_by_path + +from ayon_core.pipeline import get_current_project_name, get_current_folder_path class ToolWindows: @@ -38,22 +39,30 @@ class ToolWindows: cls._windows[tool] = window -def _resolution_from_document(doc): - if not doc or "data" not in doc: - print("Entered document is not valid. \"{}\"".format(str(doc))) +def _resolution_from_entity(entity): + if not entity: + print("Entered entity is not valid. \"{}\"".format( + str(entity) + )) return None - resolution_width = doc["data"].get("resolutionWidth") - resolution_height = doc["data"].get("resolutionHeight") + attributes = entity.get("attrib") + if attributes is None: + attributes = entity.get("data", {}) + + resolution_width = attributes.get("resolutionWidth") + resolution_height = attributes.get("resolutionHeight") # Backwards compatibility if resolution_width is None or resolution_height is None: - resolution_width = doc["data"].get("resolution_width") - resolution_height = doc["data"].get("resolution_height") + resolution_width = attributes.get("resolution_width") + resolution_height = attributes.get("resolution_height") # Make sure both width and height are set if resolution_width is None or resolution_height is None: cmds.warning( - "No resolution information found for \"{}\"".format(doc["name"]) + "No resolution information found for \"{}\"".format( + entity["name"] + ) ) return None @@ -65,20 +74,20 @@ def reset_resolution(): resolution_width = 1920 resolution_height = 1080 - # Get resolution from asset + # Get resolution from folder project_name = get_current_project_name() - asset_name = get_current_asset_name() - asset_doc = get_asset_by_name(project_name, asset_name) - resolution = _resolution_from_document(asset_doc) + folder_path = get_current_folder_path() + folder_entity = get_folder_by_path(project_name, folder_path) + resolution = _resolution_from_entity(folder_entity) # Try get resolution from project if resolution is None: # TODO go through visualParents print(( - "Asset \"{}\" does not have set resolution." + "Folder '{}' does not have set resolution." " Trying to get resolution from project" - ).format(asset_name)) - project_doc = get_project(project_name) - resolution = _resolution_from_document(project_doc) + ).format(folder_path)) + project_entity = get_project(project_name) + resolution = _resolution_from_entity(project_entity) if resolution is None: msg = "Using default resolution {}x{}" diff --git a/client/ayon_core/hosts/maya/api/lib.py b/client/ayon_core/hosts/maya/api/lib.py index 1aa2244111..7c3c739d7c 100644 --- a/client/ayon_core/hosts/maya/api/lib.py +++ b/client/ayon_core/hosts/maya/api/lib.py @@ -19,18 +19,12 @@ from six import string_types from maya import cmds, mel from maya.api import OpenMaya -from ayon_core.client import ( - get_project, - get_asset_by_name, - get_subsets, - get_last_versions, - get_representation_by_name, - get_asset_name_identifier, -) +import ayon_api + from ayon_core.settings import get_project_settings from ayon_core.pipeline import ( get_current_project_name, - get_current_asset_name, + get_current_folder_path, get_current_task_name, discover_loader_plugins, loaders_from_representation, @@ -43,7 +37,7 @@ from ayon_core.pipeline import ( AYON_CONTAINER_ID, ) from ayon_core.lib import NumberDef -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.pipeline.create import CreateContext from ayon_core.lib.profiles_filtering import filter_profiles @@ -284,16 +278,16 @@ def generate_capture_preset(instance, camera, path, width_preset = capture_preset["Resolution"]["width"] height_preset = capture_preset["Resolution"]["height"] - # Set resolution variables from asset values - asset_data = instance.data["assetEntity"]["data"] - asset_width = asset_data.get("resolutionWidth") - asset_height = asset_data.get("resolutionHeight") + # Set resolution variables from folder values + folder_attributes = instance.data["folderEntity"]["attrib"] + folder_width = folder_attributes.get("resolutionWidth") + folder_height = folder_attributes.get("resolutionHeight") review_instance_width = instance.data.get("review_width") review_instance_height = instance.data.get("review_height") # Use resolution from instance if review width/height is set # Otherwise use the resolution from preset if it has non-zero values - # Otherwise fall back to asset width x height + # Otherwise fall back to folder width x height # Else define no width, then `capture.capture` will use render resolution if review_instance_width and review_instance_height: preset["width"] = review_instance_width @@ -301,9 +295,9 @@ def generate_capture_preset(instance, camera, path, elif width_preset and height_preset: preset["width"] = width_preset preset["height"] = height_preset - elif asset_width and asset_height: - preset["width"] = asset_width - preset["height"] = asset_height + elif folder_width and folder_height: + preset["width"] = folder_width + preset["height"] = folder_height # Isolate view is requested by having objects in the set besides a # camera. If there is only 1 member it'll be the camera because we @@ -1638,7 +1632,7 @@ def get_id(node): return -def generate_ids(nodes, asset_id=None): +def generate_ids(nodes, folder_id=None): """Returns new unique ids for the given nodes. Note: This does not assign the new ids, it only generates the values. @@ -1655,27 +1649,33 @@ def generate_ids(nodes, asset_id=None): Args: nodes (list): List of nodes. - asset_id (str or bson.ObjectId): The database id for the *asset* to - generate for. When None provided the current asset in the - active session is used. + folder_id (Optional[str]): Folder id to generate id for. When None + provided current folder is used. Returns: list: A list of (node, id) tuples. """ - if asset_id is None: - # Get the asset ID from the database for the asset of current context + if folder_id is None: + # Get the folder id based on current context folder project_name = get_current_project_name() - asset_name = get_current_asset_name() - asset_doc = get_asset_by_name(project_name, asset_name, fields=["_id"]) - assert asset_doc, "No current asset found in Session" - asset_id = asset_doc['_id'] + folder_path = get_current_folder_path() + if not folder_path: + raise ValueError("Current folder path is not set") + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields=["id"] + ) + if not folder_entity: + raise ValueError(( + "Current folder '{}' was not found on the server" + ).format(folder_path)) + folder_id = folder_entity["id"] node_ids = [] for node in nodes: _, uid = str(uuid.uuid4()).rsplit("-", 1) - unique_id = "{}:{}".format(asset_id, uid) + unique_id = "{}:{}".format(folder_id, uid) node_ids.append((node, unique_id)) return node_ids @@ -1867,20 +1867,26 @@ def get_container_members(container): # region LOOKDEV -def list_looks(project_name, asset_id): - """Return all look products for the given asset +def list_looks(project_name, folder_id): + """Return all look products for the given folder. This assumes all look products start with "look*" in their names. + + Returns: + list[dict[str, Any]]: List of look products. + """ # # get all products with look leading in # the name associated with the asset # TODO this should probably look for product type 'look' instead of # checking product name that can not start with product type - subset_docs = get_subsets(project_name, asset_ids=[asset_id]) + product_entities = ayon_api.get_products( + project_name, folder_ids=[folder_id] + ) return [ - subset_doc - for subset_doc in subset_docs - if subset_doc["name"].startswith("look") + product_entity + for product_entity in product_entities + if product_entity["name"].startswith("look") ] @@ -1900,16 +1906,16 @@ def assign_look_by_version(nodes, version_id): project_name = get_current_project_name() # Get representations of shader file and relationships - look_representation = get_representation_by_name( + look_representation = ayon_api.get_representation_by_name( project_name, "ma", version_id ) - json_representation = get_representation_by_name( + json_representation = ayon_api.get_representation_by_name( project_name, "json", version_id ) # See if representation is already loaded, if so reuse it. host = registered_host() - representation_id = str(look_representation['_id']) + representation_id = look_representation["id"] for container in host.ls(): if (container['loader'] == "LookLoader" and container['representation'] == representation_id): @@ -1945,7 +1951,7 @@ def assign_look_by_version(nodes, version_id): def assign_look(nodes, product_name="lookDefault"): """Assigns a look to a node. - Optimizes the nodes by grouping by asset id and finding + Optimizes the nodes by grouping by folder id and finding related product by name. Args: @@ -1953,66 +1959,65 @@ def assign_look(nodes, product_name="lookDefault"): product_name (str): name of the product to find """ - # Group all nodes per asset id + # Group all nodes per folder id grouped = defaultdict(list) for node in nodes: - pype_id = get_id(node) - if not pype_id: + hash_id = get_id(node) + if not hash_id: continue - parts = pype_id.split(":", 1) + parts = hash_id.split(":", 1) grouped[parts[0]].append(node) project_name = get_current_project_name() - subset_docs = get_subsets( - project_name, subset_names=[product_name], asset_ids=grouped.keys() + product_entities = ayon_api.get_products( + project_name, product_names=[product_name], folder_ids=grouped.keys() ) - subset_docs_by_asset_id = { - str(subset_doc["parent"]): subset_doc - for subset_doc in subset_docs + product_entities_by_folder_id = { + product_entity["folderId"]: product_entity + for product_entity in product_entities } product_ids = { - subset_doc["_id"] - for subset_doc in subset_docs_by_asset_id.values() + product_entity["id"] + for product_entity in product_entities_by_folder_id.values() } - last_version_docs = get_last_versions( + last_version_entities = ayon_api.get_last_versions( project_name, - subset_ids=product_ids, - fields=["_id", "name", "data.families"] + product_ids ) - last_version_docs_by_product_id = { - last_version_doc["parent"]: last_version_doc - for last_version_doc in last_version_docs + last_version_entities_by_product_id = { + last_version_entity["productId"]: last_version_entity + for last_version_entity in last_version_entities } - for asset_id, asset_nodes in grouped.items(): - # create objectId for database - subset_doc = subset_docs_by_asset_id.get(asset_id) - if not subset_doc: + for folder_id, asset_nodes in grouped.items(): + product_entity = product_entities_by_folder_id.get(folder_id) + if not product_entity: log.warning(( "No product '{}' found for {}" - ).format(product_name, asset_id)) + ).format(product_name, folder_id)) continue - last_version = last_version_docs_by_product_id.get(subset_doc["_id"]) + product_id = product_entity["id"] + last_version = last_version_entities_by_product_id.get(product_id) if not last_version: log.warning(( - "Not found last version for product '{}' on asset with id {}" - ).format(product_name, asset_id)) + "Not found last version for product '{}' on folder with id {}" + ).format(product_name, folder_id)) continue - families = last_version.get("data", {}).get("families") or [] + families = last_version.get("attrib", {}).get("families") or [] if "look" not in families: log.warning(( - "Last version for product '{}' on asset with id {}" + "Last version for product '{}' on folder with id {}" " does not have look product type" - ).format(product_name, asset_id)) + ).format(product_name, folder_id)) continue log.debug("Assigning look '{}' ".format( - product_name, last_version["name"])) + product_name, last_version["version"])) - assign_look_by_version(asset_nodes, last_version["_id"]) + assign_look_by_version(asset_nodes, last_version["id"]) def apply_shaders(relationships, shadernodes, nodes): @@ -2498,14 +2503,16 @@ def get_fps_for_current_context(): """ project_name = get_current_project_name() - asset_name = get_current_asset_name() - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["data.fps"] + folder_path = get_current_folder_path() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"attrib.fps"} ) or {} - fps = asset_doc.get("data", {}).get("fps") + fps = folder_entity.get("attrib", {}).get("fps") if not fps: - project_doc = get_project(project_name, fields=["data.fps"]) or {} - fps = project_doc.get("data", {}).get("fps") + project_entity = ayon_api.get_project( + project_name, fields=["attrib.fps"] + ) or {} + fps = project_entity.get("attrib", {}).get("fps") if not fps: fps = 25 @@ -2514,7 +2521,7 @@ def get_fps_for_current_context(): def get_frame_range(include_animation_range=False): - """Get the current assets frame range and handles. + """Get the current folder frame range and handles. Args: include_animation_range (bool, optional): Whether to include @@ -2522,24 +2529,25 @@ def get_frame_range(include_animation_range=False): range of the timeline. It is excluded by default. Returns: - dict: Asset's expected frame range values. + dict: Folder's expected frame range values. """ # Set frame start/end project_name = get_current_project_name() - asset_name = get_current_asset_name() - asset = get_asset_by_name(project_name, asset_name) + folder_path = get_current_folder_path() + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + folder_attributes = folder_entity["attrib"] - frame_start = asset["data"].get("frameStart") - frame_end = asset["data"].get("frameEnd") + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") if frame_start is None or frame_end is None: - cmds.warning("No edit information found for %s" % asset_name) + cmds.warning("No edit information found for '{}'".format(folder_path)) return - handle_start = asset["data"].get("handleStart") or 0 - handle_end = asset["data"].get("handleEnd") or 0 + handle_start = folder_attributes.get("handleStart") or 0 + handle_end = folder_attributes.get("handleEnd") or 0 frame_range = { "frameStart": frame_start, @@ -2555,15 +2563,21 @@ def get_frame_range(include_animation_range=False): # keys. That is why these are excluded by default. task_name = get_current_task_name() settings = get_project_settings(project_name) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + task_type = None + if task_entity: + task_type = task_entity["taskType"] + include_handles_settings = settings["maya"]["include_handles"] - current_task = asset.get("data").get("tasks").get(task_name) animation_start = frame_start animation_end = frame_end include_handles = include_handles_settings["include_handles_default"] for item in include_handles_settings["per_task_type"]: - if current_task["type"] in item["task_type"]: + if task_type in item["task_type"]: include_handles = item["include_handles"] break if include_handles: @@ -2577,7 +2591,7 @@ def get_frame_range(include_animation_range=False): def reset_frame_range(playback=True, render=True, fps=True): - """Set frame range to current asset + """Set frame range to current folder. Args: playback (bool, Optional): Whether to set the maya timeline playback @@ -2591,7 +2605,7 @@ def reset_frame_range(playback=True, render=True, fps=True): frame_range = get_frame_range(include_animation_range=True) if not frame_range: - # No frame range data found for asset + # No frame range data found for folder return frame_start = frame_range["frameStart"] @@ -2616,27 +2630,19 @@ def reset_frame_range(playback=True, render=True, fps=True): def reset_scene_resolution(): """Apply the scene resolution from the project definition - scene resolution can be overwritten by an asset if the asset.data contains - any information regarding scene resolution . + scene resolution can be overwritten by an folder if the folder.attrib + contains any information regarding scene resolution . Returns: None """ - project_name = get_current_project_name() - project_doc = get_project(project_name) - project_data = project_doc["data"] - asset_data = get_current_project_asset()["data"] + folder_attributes = get_current_project_folder()["attrib"] - # Set project resolution - width_key = "resolutionWidth" - height_key = "resolutionHeight" - pixelAspect_key = "pixelAspect" - - width = asset_data.get(width_key, project_data.get(width_key, 1920)) - height = asset_data.get(height_key, project_data.get(height_key, 1080)) - pixelAspect = asset_data.get(pixelAspect_key, - project_data.get(pixelAspect_key, 1)) + # Set resolution + width = folder_attributes.get("resolutionWidth", 1920) + height = folder_attributes.get("resolutionHeight", 1080) + pixelAspect = folder_attributes.get("pixelAspect", 1) set_scene_resolution(width, height, pixelAspect) @@ -2644,7 +2650,7 @@ def reset_scene_resolution(): def set_context_settings(): """Apply the project settings from the project definition - Settings can be overwritten by an asset if the asset.data contains + Settings can be overwritten by an folder if the folder.attrib contains any information regarding those settings. Examples of settings: @@ -2654,9 +2660,8 @@ def set_context_settings(): Returns: None + """ - - # Set project fps set_scene_fps(get_fps_for_current_context()) @@ -3158,9 +3163,9 @@ def update_content_on_context_change(): This will update scene content to match new folder on context change """ scene_sets = cmds.listSets(allSets=True) - asset_doc = get_current_project_asset() - new_folder_path = get_asset_name_identifier(asset_doc) - new_data = asset_doc["data"] + folder_entity = get_current_project_folder() + folder_attributes = folder_entity["attrib"] + new_folder_path = folder_entity["path"] for s in scene_sets: try: if cmds.getAttr("{}.id".format(s)) in { @@ -3178,10 +3183,10 @@ def update_content_on_context_change(): ) if "frameStart" in attr: cmds.setAttr("{}.frameStart".format(s), - new_data["frameStart"]) + folder_attributes["frameStart"]) if "frameEnd" in attr: cmds.setAttr("{}.frameEnd".format(s), - new_data["frameEnd"],) + folder_attributes["frameEnd"],) except ValueError: pass @@ -3968,7 +3973,7 @@ def get_capture_preset( Args: task_name (str): Task name. task_type (str): Task type. - product_name (str): Subset name. + product_name (str): Product name. project_settings (dict): Project settings. log (logging.Logger): Logging object. """ @@ -4119,15 +4124,24 @@ def create_rig_animation_instance( ) assert roots, "No root nodes in rig, this is a bug." + folder_entity = context["folder"] + product_entity = context["product"] + product_type = product_entity["productType"] + product_name = product_entity["name"] + custom_product_name = options.get("animationProductName") if custom_product_name: formatting_data = { - "asset": context["asset"], - "subset": context['subset']['name'], - "family": ( - context['subset']['data'].get('family') or - context['subset']['data']['families'][0] - ) + "folder": { + "name": folder_entity["name"] + }, + "product": { + "type": product_type, + "name": product_name, + }, + "asset": folder_entity["name"], + "subset": product_name, + "family": product_type } namespace = get_custom_namespace( custom_product_name.format(**formatting_data) diff --git a/client/ayon_core/hosts/maya/api/lib_rendersettings.py b/client/ayon_core/hosts/maya/api/lib_rendersettings.py index b8a4d04a10..905e8c69af 100644 --- a/client/ayon_core/hosts/maya/api/lib_rendersettings.py +++ b/client/ayon_core/hosts/maya/api/lib_rendersettings.py @@ -7,7 +7,7 @@ from ayon_core.lib import Logger from ayon_core.settings import get_project_settings from ayon_core.pipeline import CreatorError, get_current_project_name -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.hosts.maya.api.lib import reset_frame_range @@ -77,7 +77,8 @@ class RenderSettings(object): renderer = cmds.getAttr( 'defaultRenderGlobals.currentRenderer').lower() - asset_doc = get_current_project_asset() + folder_entity = get_current_project_folder() + folder_attributes = folder_entity["attrib"] # project_settings/maya/create/CreateRender/aov_separator try: aov_separator = self._aov_chars[( @@ -101,8 +102,8 @@ class RenderSettings(object): else: print("{0} isn't a supported renderer to autoset settings.".format(renderer)) # noqa # TODO: handle not having res values in the doc - width = asset_doc["data"].get("resolutionWidth") - height = asset_doc["data"].get("resolutionHeight") + width = folder_attributes.get("resolutionWidth") + height = folder_attributes.get("resolutionHeight") if renderer == "arnold": # set renderer settings for Arnold from project settings diff --git a/client/ayon_core/hosts/maya/api/menu.py b/client/ayon_core/hosts/maya/api/menu.py index 70347e91b6..8de025bfe5 100644 --- a/client/ayon_core/hosts/maya/api/menu.py +++ b/client/ayon_core/hosts/maya/api/menu.py @@ -8,7 +8,7 @@ import maya.utils import maya.cmds as cmds from ayon_core.pipeline import ( - get_current_asset_name, + get_current_folder_path, get_current_task_name, registered_host ) @@ -43,7 +43,7 @@ def _get_menu(menu_name=None): def get_context_label(): return "{}, {}".format( - get_current_asset_name(), + get_current_folder_path(), get_current_task_name() ) diff --git a/client/ayon_core/hosts/maya/api/pipeline.py b/client/ayon_core/hosts/maya/api/pipeline.py index 90fb2e5888..9792a4a5fe 100644 --- a/client/ayon_core/hosts/maya/api/pipeline.py +++ b/client/ayon_core/hosts/maya/api/pipeline.py @@ -449,7 +449,7 @@ def containerise(name, ("name", name), ("namespace", namespace), ("loader", loader), - ("representation", context["representation"]["_id"]), + ("representation", context["representation"]["id"]), ] for key, value in data: @@ -588,7 +588,7 @@ def on_open(): from ayon_core.tools.utils import SimplePopup # Validate FPS after update_task_from_path to - # ensure it is using correct FPS for the asset + # ensure it is using correct FPS for the folder lib.validate_fps() lib.fix_incompatible_containers() diff --git a/client/ayon_core/hosts/maya/api/plugin.py b/client/ayon_core/hosts/maya/api/plugin.py index 1151b0e248..eaf93725f4 100644 --- a/client/ayon_core/hosts/maya/api/plugin.py +++ b/client/ayon_core/hosts/maya/api/plugin.py @@ -4,6 +4,7 @@ from abc import ABCMeta import qargparse import six +import ayon_api from maya import cmds from maya.app.renderSetup.model import renderSetup @@ -28,7 +29,6 @@ from ayon_core.pipeline import ( get_current_project_name, ) from ayon_core.pipeline.load import LoadError -from ayon_core.client import get_asset_by_name from ayon_core.pipeline.create import get_product_name from . import lib @@ -454,17 +454,23 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): # this instance will not have the `instance_node` data yet # until it's been saved/persisted at least once. project_name = self.create_context.get_current_project_name() - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() + task_name = self.create_context.get_current_task_name() instance_data = { - "folderPath": asset_name, - "task": self.create_context.get_current_task_name(), + "folderPath": folder_path, + "task": task_name, "variant": layer.name(), } - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - instance_data["task"], + folder_entity, + task_entity, layer.name(), host_name, ) @@ -578,8 +584,8 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): def get_product_name( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=None, instance=None @@ -587,13 +593,22 @@ class RenderlayerCreator(NewCreator, MayaCreatorBase): if host_name is None: host_name = self.create_context.host_name dynamic_data = self.get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] # creator.product_type != 'render' as expected return get_product_name( project_name, - asset_doc, task_name, + task_type host_name, self.layer_instance_prefix or self.product_type, variant, @@ -668,21 +683,19 @@ class Loader(LoaderPlugin): self.log.debug("No custom group_name, no group will be created.") options["attach_to_root"] = False - asset_doc = context["asset"] - subset_doc = context["subset"] - product_type = ( - subset_doc["data"].get("family") - or subset_doc["data"]["families"][0] - ) + folder_entity = context["folder"] + product_entity = context["product"] + product_name = product_entity["name"] + product_type = product_entity["productType"] formatting_data = { - "asset_name": asset_doc["name"], - "asset_type": asset_doc["type"], + "asset_name": folder_entity["name"], + "asset_type": "asset", "folder": { - "name": asset_doc["name"], + "name": folder_entity["name"], }, - "subset": subset_doc["name"], + "subset": product_name, "product": { - "name": subset_doc["name"], + "name": product_name, "type": product_type, }, "family": product_type @@ -801,9 +814,9 @@ class ReferenceLoader(Loader): node = container["objectName"] project_name = context["project"]["name"] - repre_doc = context["representation"] + repre_entity = context["representation"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) # Get reference node from container members members = get_container_members(node) @@ -816,9 +829,9 @@ class ReferenceLoader(Loader): "abc": "Alembic", "fbx": "FBX", "usd": "USD Import" - }.get(repre_doc["name"]) + }.get(repre_entity["name"]) - assert file_type, "Unsupported representation: %s" % repre_doc + assert file_type, "Unsupported representation: %s" % repre_entity assert os.path.exists(path), "%s does not exist." % path @@ -826,7 +839,7 @@ class ReferenceLoader(Loader): # them to incoming data. alembic_attrs = ["speed", "offset", "cycleType", "time"] alembic_data = {} - if repre_doc["name"] == "abc": + if repre_entity["name"] == "abc": alembic_nodes = cmds.ls( "{}:*".format(namespace), type="AlembicNode" ) @@ -867,7 +880,7 @@ class ReferenceLoader(Loader): self._organize_containers(content, container["objectName"]) # Reapply alembic settings. - if repre_doc["name"] == "abc" and alembic_data: + if repre_entity["name"] == "abc" and alembic_data: alembic_nodes = cmds.ls( "{}:*".format(namespace), type="AlembicNode" ) @@ -901,7 +914,7 @@ class ReferenceLoader(Loader): # Update metadata cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") # When an animation or pointcache gets connected to an Xgen container, diff --git a/client/ayon_core/hosts/maya/api/setdress.py b/client/ayon_core/hosts/maya/api/setdress.py index 913e12fd3f..7276ae254c 100644 --- a/client/ayon_core/hosts/maya/api/setdress.py +++ b/client/ayon_core/hosts/maya/api/setdress.py @@ -6,16 +6,10 @@ import contextlib import copy import six +import ayon_api from maya import cmds -from ayon_core.client import ( - get_version_by_name, - get_last_version_by_subset_id, - get_representation_by_id, - get_representation_by_name, - get_representation_parents, -) from ayon_core.pipeline import ( schema, discover_loader_plugins, @@ -290,41 +284,43 @@ def update_package_version(container, version): # Versioning (from `core.maya.pipeline`) project_name = get_current_project_name() - current_representation = get_representation_by_id( - project_name, container["representation"] + repre_id = container["representation"] + current_representation = ayon_api.get_representation_by_id( + project_name, repre_id ) assert current_representation is not None, "This is a bug" - version_doc, subset_doc, asset_doc, project_doc = ( - get_representation_parents(project_name, current_representation) - ) + ( + version_entity, + product_entity, + folder_entity, + project_entity + ) = ayon_api.get_representation_parents(project_name, repre_id) if version == -1: - new_version = get_last_version_by_subset_id( - project_name, subset_doc["_id"] + new_version = ayon_api.get_last_version_by_product_id( + project_name, product_entity["id"] ) else: - new_version = get_version_by_name( - project_name, version, subset_doc["_id"] + new_version = ayon_api.get_version_by_name( + project_name, version, product_entity["id"] ) - assert new_version is not None, "This is a bug" + if new_version is None: + raise ValueError("Version not found: {}".format(version)) # Get the new representation (new file) - new_representation = get_representation_by_name( - project_name, current_representation["name"], new_version["_id"] + new_representation = ayon_api.get_representation_by_name( + project_name, current_representation["name"], new_version["id"] ) # TODO there is 'get_representation_context' to get the context which # could be possible to use here new_context = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code", "") - }, - "asset": asset_doc, - "subset": subset_doc, - "version": version_doc, + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, "representation": new_representation, } update_package(container, new_context) @@ -344,8 +340,8 @@ def update_package(set_container, context): # Load the original package data project_name = context["project"]["name"] - repre_doc = context["representation"] - current_representation = get_representation_by_id( + repre_entity = context["representation"] + current_representation = ayon_api.get_representation_by_id( project_name, set_container["representation"] ) @@ -355,7 +351,7 @@ def update_package(set_container, context): current_data = json.load(fp) # Load the new package data - new_file = get_representation_path(repre_doc) + new_file = get_representation_path(repre_entity) assert new_file.endswith(".json") with open(new_file, "r") as fp: new_data = json.load(fp) @@ -366,7 +362,7 @@ def update_package(set_container, context): # TODO: This should be handled by the pipeline itself cmds.setAttr(set_container['objectName'] + ".representation", - str(repre_doc['_id']), type="string") + context["representation"]["id"], type="string") def update_scene(set_container, containers, current_data, new_data, new_file): @@ -413,6 +409,8 @@ def update_scene(set_container, containers, current_data, new_data, new_file): new_lookup = _instances_by_namespace(new_data) old_lookup = _instances_by_namespace(current_data) + repre_ids = set() + containers_for_repre_compare = [] for container in containers: container_ns = container['namespace'] @@ -421,98 +419,121 @@ def update_scene(set_container, containers, current_data, new_data, new_file): processed_namespaces.add(container_ns) processed_containers.append(container['objectName']) - if container_ns in new_lookup: - root = get_container_transforms(container, root=True) - if not root: - log.error("Can't find root for %s", container['objectName']) - continue - - old_instance = old_lookup.get(container_ns, {}) - new_instance = new_lookup[container_ns] - - # Update the matrix - # check matrix against old_data matrix to find local overrides - current_matrix = cmds.xform(root, - query=True, - matrix=True, - objectSpace=True) - - original_matrix = old_instance.get("matrix", identity) - has_matrix_override = not matrix_equals(current_matrix, - original_matrix) - - if has_matrix_override: - log.warning("Matrix override preserved on %s", container_ns) - else: - new_matrix = new_instance.get("matrix", identity) - cmds.xform(root, matrix=new_matrix, objectSpace=True) - - # Update the parenting - if old_instance.get("parent", None) != new_instance["parent"]: - - parent = to_namespace(new_instance['parent'], set_namespace) - if not cmds.objExists(parent): - log.error("Can't find parent %s", parent) - continue - - # Set the new parent - cmds.lockNode(root, lock=False) - root = cmds.parent(root, parent, relative=True) - cmds.lockNode(root, lock=True) - - # Update the representation - representation_current = container['representation'] - representation_old = old_instance['representation'] - representation_new = new_instance['representation'] - has_representation_override = (representation_current != - representation_old) - - if representation_new != representation_current: - - if has_representation_override: - log.warning("Your scene had local representation " - "overrides within the set. New " - "representations not loaded for %s.", - container_ns) - continue - - # We check it against the current 'loader' in the scene instead - # of the original data of the package that was loaded because - # an Artist might have made scene local overrides - if new_instance['loader'] != container['loader']: - log.warning("Loader is switched - local edits will be " - "lost. Removing: %s", - container_ns) - - # Remove this from the "has been processed" list so it's - # considered as new element and added afterwards. - processed_containers.pop() - processed_namespaces.remove(container_ns) - remove_container(container) - continue - - # Check whether the conversion can be done by the Loader. - # They *must* use the same asset, product and Loader for - # `update_container` to make sense. - old = get_representation_by_id( - project_name, representation_current - ) - new = get_representation_by_id( - project_name, representation_new - ) - is_valid = compare_representations(old=old, new=new) - if not is_valid: - log.error("Skipping: %s. See log for details.", - container_ns) - continue - - new_version = new["context"]["version"] - update_container(container, version=new_version) - - else: + if container_ns not in new_lookup: # Remove this container because it's not in the new data log.warning("Removing content: %s", container_ns) remove_container(container) + continue + + root = get_container_transforms(container, root=True) + if not root: + log.error("Can't find root for %s", container['objectName']) + continue + + old_instance = old_lookup.get(container_ns, {}) + new_instance = new_lookup[container_ns] + + # Update the matrix + # check matrix against old_data matrix to find local overrides + current_matrix = cmds.xform(root, + query=True, + matrix=True, + objectSpace=True) + + original_matrix = old_instance.get("matrix", identity) + has_matrix_override = not matrix_equals(current_matrix, + original_matrix) + + if has_matrix_override: + log.warning("Matrix override preserved on %s", container_ns) + else: + new_matrix = new_instance.get("matrix", identity) + cmds.xform(root, matrix=new_matrix, objectSpace=True) + + # Update the parenting + if old_instance.get("parent", None) != new_instance["parent"]: + + parent = to_namespace(new_instance['parent'], set_namespace) + if not cmds.objExists(parent): + log.error("Can't find parent %s", parent) + continue + + # Set the new parent + cmds.lockNode(root, lock=False) + root = cmds.parent(root, parent, relative=True) + cmds.lockNode(root, lock=True) + + # Update the representation + representation_current = container['representation'] + representation_old = old_instance['representation'] + representation_new = new_instance['representation'] + has_representation_override = (representation_current != + representation_old) + + if representation_new == representation_current: + continue + + if has_representation_override: + log.warning("Your scene had local representation " + "overrides within the set. New " + "representations not loaded for %s.", + container_ns) + continue + + # We check it against the current 'loader' in the scene instead + # of the original data of the package that was loaded because + # an Artist might have made scene local overrides + if new_instance['loader'] != container['loader']: + log.warning("Loader is switched - local edits will be " + "lost. Removing: %s", + container_ns) + + # Remove this from the "has been processed" list so it's + # considered as new element and added afterwards. + processed_containers.pop() + processed_namespaces.remove(container_ns) + remove_container(container) + continue + + # Check whether the conversion can be done by the Loader. + # They *must* use the same folder, product and Loader for + # `update_container` to make sense. + repre_ids.add(representation_current) + repre_ids.add(representation_new) + + containers_for_repre_compare.append( + (container, representation_current, representation_new) + ) + + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in ayon_api.get_representations( + project_name, representation_ids=repre_ids + ) + } + repre_parents_by_id = ayon_api.get_representations_parents( + project_name, repre_ids + ) + for ( + container, + repre_current_id, + repre_new_id + ) in containers_for_repre_compare: + current_repre = repre_entities_by_id[repre_current_id] + current_parents = repre_parents_by_id[repre_current_id] + new_repre = repre_entities_by_id[repre_new_id] + new_parents = repre_parents_by_id[repre_new_id] + + is_valid = compare_representations( + current_repre, current_parents, new_repre, new_parents + ) + if not is_valid: + log.error("Skipping: %s. See log for details.", + container["namespace"]) + continue + + new_version = new_parents.version["version"] + update_container(container, version=new_version) # Add new assets all_loaders = discover_loader_plugins() @@ -543,43 +564,43 @@ def update_scene(set_container, containers, current_data, new_data, new_file): return processed_containers -def compare_representations(old, new): +def compare_representations( + current_repre, current_parents, new_repre, new_parents +): """Check if the old representation given can be updated Due to limitations of the `update_container` function we cannot allow differences in the following data: * Representation name (extension) - * Asset name - * Subset name (variation) + * Folder id + * Product id If any of those data values differs, the function will raise an RuntimeError Args: - old(dict): representation data from the database - new(dict): representation data from the database + current_repre (dict[str, Any]): Current representation entity. + current_parents (RepresentationParents): Current + representation parents. + new_repre (dict[str, Any]): New representation entity. + new_parents (RepresentationParents): New representation parents. Returns: bool: False if the representation is not invalid else True - """ - if new["name"] != old["name"]: + """ + if current_repre["name"] != new_repre["name"]: log.error("Cannot switch extensions") return False - new_context = new["context"] - old_context = old["context"] - # TODO add better validation e.g. based on parent ids - if new_context["asset"] != old_context["asset"]: - log.error("Changing assets between updates is " - "not supported.") + if current_parents.folder["id"] != new_parents.folder["id"]: + log.error("Changing folders between updates is not supported.") return False - if new_context["subset"] != old_context["subset"]: - log.error("Changing products between updates is " - "not supported.") + if current_parents.product["id"] != new_parents.product["id"]: + log.error("Changing products between updates is not supported.") return False return True diff --git a/client/ayon_core/hosts/maya/api/workfile_template_builder.py b/client/ayon_core/hosts/maya/api/workfile_template_builder.py index 6ae2a075e3..cb25a722f0 100644 --- a/client/ayon_core/hosts/maya/api/workfile_template_builder.py +++ b/client/ayon_core/hosts/maya/api/workfile_template_builder.py @@ -4,7 +4,7 @@ from maya import cmds from ayon_core.pipeline import ( registered_host, - get_current_asset_name, + get_current_folder_path, AYON_INSTANCE_ID, AVALON_INSTANCE_ID, ) @@ -74,7 +74,7 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): return True # update imported sets information - asset_name = get_current_asset_name() + folder_path = get_current_folder_path() for node in imported_sets: if not cmds.attributeQuery("id", node=node, exists=True): continue @@ -82,11 +82,11 @@ class MayaTemplateBuilder(AbstractTemplateBuilder): AYON_INSTANCE_ID, AVALON_INSTANCE_ID }: continue - if not cmds.attributeQuery("asset", node=node, exists=True): + if not cmds.attributeQuery("folderPath", node=node, exists=True): continue cmds.setAttr( - "{}.asset".format(node), asset_name, type="string") + "{}.folderPath".format(node), folder_path, type="string") return True diff --git a/client/ayon_core/hosts/maya/hooks/pre_copy_mel.py b/client/ayon_core/hosts/maya/hooks/pre_copy_mel.py index 03ca8661bd..3fd81ceff4 100644 --- a/client/ayon_core/hosts/maya/hooks/pre_copy_mel.py +++ b/client/ayon_core/hosts/maya/hooks/pre_copy_mel.py @@ -11,11 +11,13 @@ class PreCopyMel(PreLaunchHook): launch_types = {LaunchTypes.local} def execute(self): - project_doc = self.data["project_doc"] + project_entity = self.data["project_entity"] workdir = self.launch_context.env.get("AYON_WORKDIR") if not workdir: self.log.warning("BUG: Workdir is not filled.") return project_settings = self.data["project_settings"] - create_workspace_mel(workdir, project_doc["name"], project_settings) + create_workspace_mel( + workdir, project_entity["name"], project_settings + ) diff --git a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py index b23c56fc5b..9907b5f340 100644 --- a/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/maya/plugins/create/convert_legacy.py @@ -1,14 +1,14 @@ -from ayon_core.pipeline.create.creator_plugins import SubsetConvertorPlugin +import ayon_api + +from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin from ayon_core.hosts.maya.api import plugin from ayon_core.hosts.maya.api.lib import read -from ayon_core.client import get_asset_by_name - from maya import cmds from maya.app.renderSetup.model import renderSetup -class MayaLegacyConvertor(SubsetConvertorPlugin, +class MayaLegacyConvertor(ProductConvertorPlugin, plugin.MayaCreatorBase): """Find and convert any legacy products in the scene. @@ -142,12 +142,21 @@ class MayaLegacyConvertor(SubsetConvertorPlugin, # recreate product name as without it would be # `renderingMain` vs correct `renderMain` project_name = self.create_context.get_current_project_name() - asset_doc = get_asset_by_name(project_name, - original_data["asset"]) + folder_entities = list(ayon_api.get_folders( + project_name, folder_names=[original_data["asset"]] + )) + if not folder_entities: + cmds.delete(instance_node) + continue + folder_entity = folder_entities[0] + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], data["task"] + ) + product_name = creator.get_product_name( project_name, - asset_doc, - data["task"], + folder_entity, + task_entity, original_data["variant"], ) original_data["productName"] = product_name diff --git a/client/ayon_core/hosts/maya/plugins/create/create_multishot_layout.py b/client/ayon_core/hosts/maya/plugins/create/create_multishot_layout.py index e7b903312f..7216236719 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_multishot_layout.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_multishot_layout.py @@ -1,16 +1,18 @@ +import collections + from ayon_api import ( get_folder_by_name, get_folder_by_path, get_folders, + get_tasks, ) from maya import cmds # noqa: F401 -from ayon_core.client import get_assets from ayon_core.hosts.maya.api import plugin from ayon_core.lib import BoolDef, EnumDef, TextDef from ayon_core.pipeline import ( Creator, - get_current_asset_name, + get_current_folder_path, get_current_project_name, ) from ayon_core.pipeline.create import CreatorError @@ -39,13 +41,13 @@ class CreateMultishotLayout(plugin.MayaCreator): Todo: `get_folder_by_name` should be switched to `get_folder_by_path` once the fork to pure AYON is done. - Warning: this will not work for projects where the asset name + Warning: this will not work for projects where the folder name is not unique across the project until the switch mentioned above is done. """ project_name = get_current_project_name() - folder_path = get_current_asset_name() + folder_path = get_current_folder_path() if "/" in folder_path: current_folder = get_folder_by_path(project_name, folder_path) else: @@ -128,10 +130,18 @@ class CreateMultishotLayout(plugin.MayaCreator): raise CreatorError( f"Creator {layout_creator_id} not found.") - # Get OpenPype style asset documents for the shots - op_asset_docs = get_assets( - self.project_name, [s["id"] for s in shots]) - asset_docs_by_id = {doc["_id"]: doc for doc in op_asset_docs} + folder_ids = {s["id"] for s in shots} + folder_entities = get_folders(self.project_name, folder_ids) + task_entities = get_tasks( + self.project_name, folder_ids=folder_ids + ) + task_entities_by_folder_id = collections.defaultdict(dict) + for task_entity in task_entities: + folder_id = task_entity["folderId"] + task_name = task_entity["name"] + task_entities_by_folder_id[folder_id][task_name] = task_entity + + folder_entities_by_id = {fe["id"]: fe for fe in folder_entities} for shot in shots: # we are setting shot name to be displayed in the sequencer to # `shot name (shot label)` if the label is set, otherwise just @@ -141,12 +151,15 @@ class CreateMultishotLayout(plugin.MayaCreator): continue # get task for shot - asset_doc = asset_docs_by_id[shot["id"]] + folder_id = shot["id"] + folder_entity = folder_entities_by_id[folder_id] + task_entities = task_entities_by_folder_id[folder_id] - tasks = asset_doc.get("data").get("tasks").keys() - layout_task = None - if pre_create_data["taskName"] in tasks: - layout_task = pre_create_data["taskName"] + layout_task_name = None + layout_task_entity = None + if pre_create_data["taskName"] in task_entities: + layout_task_name = pre_create_data["taskName"] + layout_task_entity = task_entities[layout_task_name] shot_name = f"{shot['name']}%s" % ( f" ({shot['label']})" if shot["label"] else "") @@ -160,14 +173,14 @@ class CreateMultishotLayout(plugin.MayaCreator): "folderPath": shot["path"], "variant": layout_creator.get_default_variant() } - if layout_task: - instance_data["task"] = layout_task + if layout_task_name: + instance_data["task"] = layout_task_name layout_creator.create( product_name=layout_creator.get_product_name( self.project_name, - asset_doc, - self.create_context.get_current_task_name(), + folder_entity, + layout_task_entity, layout_creator.get_default_variant(), ), instance_data=instance_data, @@ -177,7 +190,7 @@ class CreateMultishotLayout(plugin.MayaCreator): ) def get_related_shots(self, folder_path: str): - """Get all shots related to the current asset. + """Get all shots related to the current folder. Get all folders of type Shot under specified folder. diff --git a/client/ayon_core/hosts/maya/plugins/create/create_review.py b/client/ayon_core/hosts/maya/plugins/create/create_review.py index c4fa045427..8a2f2df745 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_review.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_review.py @@ -1,6 +1,7 @@ import json from maya import cmds +import ayon_api from ayon_core.hosts.maya.api import ( lib, @@ -12,7 +13,6 @@ from ayon_core.lib import ( EnumDef ) from ayon_core.pipeline import CreatedInstance -from ayon_core.client import get_asset_by_name TRANSPARENCIES = [ "preset", @@ -43,12 +43,17 @@ class CreateReview(plugin.MayaCreator): members = cmds.ls(selection=True) project_name = self.project_name - asset_name = instance_data["folderPath"] - asset_doc = get_asset_by_name(project_name, asset_name) + folder_path = instance_data["folderPath"] task_name = instance_data["task"] + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name, fields={"taskType"} + ) preset = lib.get_capture_preset( task_name, - asset_doc["data"]["tasks"][task_name]["type"], + task_entity["taskType"], product_name, self.project_settings, self.log @@ -91,9 +96,9 @@ class CreateReview(plugin.MayaCreator): defs = lib.collect_animation_defs() - # Option for using Maya or asset frame range in settings. + # Option for using Maya or folder frame range in settings. if not self.useMayaTimeline: - # Update the defaults to be the asset frame range + # Update the defaults to be the folder frame range frame_range = lib.get_frame_range() defs_by_key = {attr_def.key: attr_def for attr_def in defs} for key, value in frame_range.items(): @@ -106,13 +111,13 @@ class CreateReview(plugin.MayaCreator): defs.extend([ NumberDef("review_width", label="Review width", - tooltip="A value of zero will use the asset resolution.", + tooltip="A value of zero will use the folder resolution.", decimals=0, minimum=0, default=0), NumberDef("review_height", label="Review height", - tooltip="A value of zero will use the asset resolution.", + tooltip="A value of zero will use the folder resolution.", decimals=0, minimum=0, default=0), diff --git a/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py b/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py index 8815c4d23d..9ded28b812 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_unreal_skeletalmesh.py @@ -28,16 +28,27 @@ class CreateUnrealSkeletalMesh(plugin.MayaCreator): self.joint_hints = set(settings.get("joint_hints", [])) def get_dynamic_data( - self, project_name, asset_doc, task_name, variant, host_name, instance + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ): """ The default product name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ dynamic_data = super(CreateUnrealSkeletalMesh, self).get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) - dynamic_data["asset"] = asset_doc["name"] + dynamic_data["asset"] = folder_entity["name"] return dynamic_data def create(self, product_name, instance_data, pre_create_data): diff --git a/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py b/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py index 58ad1e4133..1991f92915 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_unreal_staticmesh.py @@ -21,16 +21,27 @@ class CreateUnrealStaticMesh(plugin.MayaCreator): self.collision_prefixes = settings["collision_prefixes"] def get_dynamic_data( - self, project_name, asset_doc, task_name, variant, host_name, instance + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ): """ The default product name templates for Unreal include {asset} and thus we should pass that along as dynamic data. """ dynamic_data = super(CreateUnrealStaticMesh, self).get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) - dynamic_data["asset"] = asset_doc["name"] + dynamic_data["asset"] = folder_entity["name"] return dynamic_data def create(self, product_name, instance_data, pre_create_data): diff --git a/client/ayon_core/hosts/maya/plugins/create/create_workfile.py b/client/ayon_core/hosts/maya/plugins/create/create_workfile.py index 5eb32e1c90..f636ed7b74 100644 --- a/client/ayon_core/hosts/maya/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/maya/plugins/create/create_workfile.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +import ayon_api + from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.client import get_asset_by_name, get_asset_name_identifier from ayon_core.hosts.maya.api import plugin from maya import cmds @@ -25,34 +26,38 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): ), None) project_name = self.project_name - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - if current_instance is None: - current_instance_asset = None - else: - current_instance_asset = current_instance["folderPath"] + current_folder_path = None + if current_instance is not None: + current_folder_path = current_instance["folderPath"] if current_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": variant } data.update( self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, current_instance) @@ -63,21 +68,25 @@ class CreateWorkfile(plugin.MayaCreatorBase, AutoCreator): ) self._add_instance_to_context(current_instance) elif ( - current_instance_asset != asset_name + current_folder_path != folder_path or current_instance["task"] != task_name ): # Update instance context if is not the same - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) - asset_name = get_asset_name_identifier(asset_doc) - current_instance["folderPath"] = asset_name + current_instance["folderPath"] = folder_entity["path"] current_instance["task"] = task_name current_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/maya/plugins/inventory/connect_geometry.py b/client/ayon_core/hosts/maya/plugins/inventory/connect_geometry.py index 054c84bea2..839a4dad90 100644 --- a/client/ayon_core/hosts/maya/plugins/inventory/connect_geometry.py +++ b/client/ayon_core/hosts/maya/plugins/inventory/connect_geometry.py @@ -1,6 +1,6 @@ from maya import cmds -from ayon_core.pipeline import InventoryAction, get_representation_context +from ayon_core.pipeline import InventoryAction, get_repres_contexts from ayon_core.hosts.maya.api.lib import get_id @@ -28,14 +28,19 @@ class ConnectGeometry(InventoryAction): # Categorize containers by family. containers_by_product_type = {} + repre_ids = { + container["representation"] + for container in containers + } + repre_contexts_by_id = get_repres_contexts(repre_ids) for container in containers: - product_type = get_representation_context( - container["representation"] - )["subset"]["data"]["family"] - try: - containers_by_product_type[product_type].append(container) - except KeyError: - containers_by_product_type[product_type] = [container] + repre_id = container["representation"] + repre_context = repre_contexts_by_id[repre_id] + + product_type = repre_context["prouct"]["productType"] + + containers_by_product_type.setdefault(product_type, []) + containers_by_product_type[product_type].append(container) # Validate to only 1 source container. source_containers = containers_by_product_type.get("animation", []) diff --git a/client/ayon_core/hosts/maya/plugins/inventory/connect_xgen.py b/client/ayon_core/hosts/maya/plugins/inventory/connect_xgen.py index fa6440fc37..bf9e679928 100644 --- a/client/ayon_core/hosts/maya/plugins/inventory/connect_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/inventory/connect_xgen.py @@ -2,7 +2,9 @@ from maya import cmds import xgenm from ayon_core.pipeline import ( - InventoryAction, get_representation_context, get_representation_path + InventoryAction, + get_repres_contexts, + get_representation_path, ) @@ -25,14 +27,19 @@ class ConnectXgen(InventoryAction): # Categorize containers by product type. containers_by_product_type = {} + repre_ids = { + container["representation"] + for container in containers + } + repre_contexts_by_id = get_repres_contexts(repre_ids) for container in containers: - product_type = get_representation_context( - container["representation"] - )["subset"]["data"]["family"] - try: - containers_by_product_type[product_type].append(container) - except KeyError: - containers_by_product_type[product_type] = [container] + repre_id = container["representation"] + repre_context = repre_contexts_by_id[repre_id] + + product_type = repre_context["prouct"]["productType"] + + containers_by_product_type.setdefault(product_type, []) + containers_by_product_type[product_type].append(container) # Validate to only 1 source container. source_containers = containers_by_product_type.get("animation", []) @@ -51,13 +58,12 @@ class ConnectXgen(InventoryAction): return source_container = source_containers[0] + source_repre_id = source_container["representation"] source_object = source_container["objectName"] # Validate source representation is an alembic. source_path = get_representation_path( - get_representation_context( - source_container["representation"] - )["representation"] + repre_contexts_by_id[source_repre_id]["representation"] ).replace("\\", "/") message = "Animation container \"{}\" is not an alembic:\n{}".format( source_container["namespace"], source_path diff --git a/client/ayon_core/hosts/maya/plugins/inventory/connect_yeti_rig.py b/client/ayon_core/hosts/maya/plugins/inventory/connect_yeti_rig.py index 66807e9d5d..5916bf7b97 100644 --- a/client/ayon_core/hosts/maya/plugins/inventory/connect_yeti_rig.py +++ b/client/ayon_core/hosts/maya/plugins/inventory/connect_yeti_rig.py @@ -5,7 +5,9 @@ from collections import defaultdict from maya import cmds from ayon_core.pipeline import ( - InventoryAction, get_representation_context, get_representation_path + InventoryAction, + get_repres_contexts, + get_representation_path, ) from ayon_core.hosts.maya.api.lib import get_container_members, get_id @@ -28,10 +30,18 @@ class ConnectYetiRig(InventoryAction): # Categorize containers by product type. containers_by_product_type = defaultdict(list) + repre_ids = { + container["representation"] + for container in containers + } + repre_contexts_by_id = get_repres_contexts(repre_ids) for container in containers: - product_type = get_representation_context( - container["representation"] - )["subset"]["data"]["family"] + repre_id = container["representation"] + repre_context = repre_contexts_by_id[repre_id] + + product_type = repre_context["prouct"]["productType"] + + containers_by_product_type.setdefault(product_type, []) containers_by_product_type[product_type].append(container) # Validate to only 1 source container. @@ -66,11 +76,10 @@ class ConnectYetiRig(InventoryAction): for container in yeti_rig_containers: target_ids.update(self.nodes_by_id(container)) + repre_id = container["representation"] maya_file = get_representation_path( - get_representation_context( - container["representation"] - )["representation"] + repre_contexts_by_id[repre_id]["representation"] ) _, ext = os.path.splitext(maya_file) settings_file = maya_file.replace(ext, ".rigsettings") diff --git a/client/ayon_core/hosts/maya/plugins/inventory/import_modelrender.py b/client/ayon_core/hosts/maya/plugins/inventory/import_modelrender.py index e2cac22836..4655017ae5 100644 --- a/client/ayon_core/hosts/maya/plugins/inventory/import_modelrender.py +++ b/client/ayon_core/hosts/maya/plugins/inventory/import_modelrender.py @@ -1,13 +1,11 @@ import re import json -from ayon_core.client import ( - get_representation_by_id, - get_representations -) +import ayon_api + +from ayon_core.pipeline.load import get_representation_contexts_by_ids from ayon_core.pipeline import ( InventoryAction, - get_representation_context, get_current_project_name, ) from ayon_core.hosts.maya.api.lib import ( @@ -35,7 +33,69 @@ class ImportModelRender(InventoryAction): def process(self, containers): from maya import cmds # noqa: F401 + # --- Query entities that will be used --- project_name = get_current_project_name() + # Collect representation ids from all containers + repre_ids = { + container["representation"] + for container in containers + } + # Create mapping of representation id to version id + # - used in containers loop + version_id_by_repre_id = { + repre_entity["id"]: repre_entity["versionId"] + for repre_entity in ayon_api.get_representations( + project_name, + representation_ids=repre_ids, + fields={"id", "versionId"} + ) + } + + # Find all representations of the versions + version_ids = set(version_id_by_repre_id.values()) + repre_entities = ayon_api.get_representations( + project_name, + version_ids=version_ids, + fields={"id", "name", "versionId"} + ) + repre_entities_by_version_id = { + version_id: [] + for version_id in version_ids + } + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + repre_entities_by_version_id[version_id].append(repre_entity) + + look_repres_by_version_id = {} + look_repre_ids = set() + for version_id, repre_entities in ( + repre_entities_by_version_id.items() + ): + json_repre = None + look_repres = [] + scene_type_regex = re.compile(self.scene_type_regex) + for repre_entity in repre_entities: + repre_name = repre_entity["name"] + if repre_name == self.look_data_type: + json_repre = repre_entity + + elif scene_type_regex.fullmatch(repre_name): + look_repres.append(repre_entity) + + look_repre = look_repres[0] if look_repres else None + if look_repre: + look_repre_ids.add(look_repre["id"]) + if json_repre: + look_repre_ids.add(json_repre["id"]) + + look_repres_by_version_id[version_id] = (json_repre, look_repre) + + contexts_by_repre_id = get_representation_contexts_by_ids( + project_name, look_repre_ids + ) + + # --- Real process logic --- + # Loop over containers and assign the looks for container in containers: con_name = container["objectName"] nodes = [] @@ -45,22 +105,34 @@ class ImportModelRender(InventoryAction): else: nodes.append(n) - repr_doc = get_representation_by_id( - project_name, container["representation"], fields=["parent"] - ) - version_id = repr_doc["parent"] + repre_id = container["representation"] + version_id = version_id_by_repre_id.get(repre_id) + if version_id is None: + print("Representation '{}' was not found".format(repre_id)) + continue + + json_repre, look_repre = look_repres_by_version_id[version_id] print("Importing render sets for model %r" % con_name) - self.assign_model_render_by_version(nodes, version_id) + self._assign_model_render( + nodes, json_repre, look_repre, contexts_by_repre_id + ) - def assign_model_render_by_version(self, nodes, version_id): + def _assign_model_render( + self, nodes, json_repre, look_repre, contexts_by_repre_id + ): """Assign nodes a specific published model render data version by id. This assumes the nodes correspond with the asset. Args: - nodes(list): nodes to assign render data to - version_id (bson.ObjectId): database id of the version of model + nodes (list): nodes to assign render data to + json_repre (dict[str, Any]): Representation entity of the json + file. + look_repre (dict[str, Any]): First representation entity of the + look files. + contexts_by_repre_id (dict[str, Any]): Mapping of representation + id to its context. Returns: None @@ -68,33 +140,17 @@ class ImportModelRender(InventoryAction): from maya import cmds # noqa: F401 - project_name = get_current_project_name() - repre_docs = get_representations( - project_name, version_ids=[version_id], fields=["_id", "name"] - ) - # Get representations of shader file and relationships - json_repre = None - look_repres = [] - scene_type_regex = re.compile(self.scene_type_regex) - for repre_doc in repre_docs: - repre_name = repre_doc["name"] - if repre_name == self.look_data_type: - json_repre = repre_doc - continue - - if scene_type_regex.fullmatch(repre_name): - look_repres.append(repre_doc) - - look_repre = look_repres[0] if look_repres else None # QUESTION shouldn't be json representation validated too? if not look_repre: print("No model render sets for this model version..") return - context = get_representation_context(look_repre["_id"]) + # TODO use 'get_representation_path_with_anatomy' instead + # of 'filepath_from_context' + context = contexts_by_repre_id.get(look_repre["id"]) maya_file = self.filepath_from_context(context) - context = get_representation_context(json_repre["_id"]) + context = contexts_by_repre_id.get(json_repre["id"]) json_file = self.filepath_from_context(context) # Import the look file diff --git a/client/ayon_core/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py b/client/ayon_core/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py index 36d9864e99..cbff293cd7 100644 --- a/client/ayon_core/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py +++ b/client/ayon_core/hosts/maya/plugins/inventory/rig_recreate_animation_instance.py @@ -1,7 +1,8 @@ from ayon_core.pipeline import ( InventoryAction, - get_representation_context + get_current_project_name, ) +from ayon_core.pipeline.load import get_representation_contexts_by_ids from ayon_core.hosts.maya.api.lib import ( create_rig_animation_instance, get_container_members, @@ -23,13 +24,21 @@ class RecreateRigAnimationInstance(InventoryAction): ) def process(self, containers): + project_name = get_current_project_name() + repre_ids = { + container["representation"] + for container in containers + } + contexts_by_repre_id = get_representation_contexts_by_ids( + project_name, repre_ids + ) for container in containers: # todo: delete an existing entry if it exist or skip creation namespace = container["namespace"] - representation_id = container["representation"] - context = get_representation_context(representation_id) + repre_id = container["representation"] + context = contexts_by_repre_id[repre_id] nodes = get_container_members(container) create_rig_animation_instance(nodes, context, namespace) diff --git a/client/ayon_core/hosts/maya/plugins/load/actions.py b/client/ayon_core/hosts/maya/plugins/load/actions.py index f979623544..5f4095eeec 100644 --- a/client/ayon_core/hosts/maya/plugins/load/actions.py +++ b/client/ayon_core/hosts/maya/plugins/load/actions.py @@ -28,11 +28,9 @@ class SetFrameRangeLoader(load.LoaderPlugin): import maya.cmds as cmds - version = context['version'] - version_data = version.get("data", {}) - - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + version_attributes = context["version"]["attrib"] + start = version_attributes.get("frameStart") + end = version_attributes.get("frameEnd") if start is None or end is None: print("Skipping setting frame range because start or " @@ -63,11 +61,10 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): import maya.cmds as cmds - version = context['version'] - version_data = version.get("data", {}) + version_attributes = context["version"]["attrib"] - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + start = version_attributes.get("frameStart") + end = version_attributes.get("frameEnd") if start is None or end is None: print("Skipping setting frame range because start or " @@ -75,8 +72,8 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): return # Include handles - start -= version_data.get("handleStart", 0) - end += version_data.get("handleEnd", 0) + start -= version_attributes.get("handleStart", 0) + end += version_attributes.get("handleEnd", 0) cmds.playbackOptions(minTime=start, maxTime=end, diff --git a/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py b/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py index 8fd3ad4979..0219a72515 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_arnold_standin.py @@ -49,15 +49,14 @@ class ArnoldStandinLoader(load.LoaderPlugin): import mtoa.ui.arnoldmenu - version = context['version'] - version_data = version.get("data", {}) + version_attributes = context["version"]["attrib"] - self.log.info("version_data: {}\n".format(version_data)) + self.log.info("version_attributes: {}\n".format(version_attributes)) - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -95,7 +94,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): sequence = is_sequence(os.listdir(os.path.dirname(repre_path))) cmds.setAttr(standin_shape + ".useFrameExtension", sequence) - fps = float(version["data"].get("fps")) or 25 + fps = float(version_attributes.get("fps")) or 25 cmds.setAttr(standin_shape + ".abcFPS", fps) nodes = [root, standin, standin_shape] @@ -190,8 +189,8 @@ class ArnoldStandinLoader(load.LoaderPlugin): if cmds.nodeType(shapes[0]) == "aiStandIn": standin = shapes[0] - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) proxy_basename, proxy_path = self._get_proxy_path(path) # Whether there is proxy or so, we still update the string operator. @@ -217,7 +216,7 @@ class ArnoldStandinLoader(load.LoaderPlugin): cmds.setAttr( container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string" ) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_assembly.py b/client/ayon_core/hosts/maya/plugins/load/load_assembly.py index 1f06655dad..7ab7fc4207 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_assembly.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_assembly.py @@ -21,11 +21,10 @@ class AssemblyLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_audio.py b/client/ayon_core/hosts/maya/plugins/load/load_audio.py index df811a585c..ee37a7ad8c 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_audio.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_audio.py @@ -30,10 +30,10 @@ class AudioLoader(load.LoaderPlugin): displaySound=True ) - asset = context["asset"]["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -46,7 +46,7 @@ class AudioLoader(load.LoaderPlugin): ) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] members = get_container_members(container) audio_nodes = cmds.ls(members, type="audio") @@ -61,7 +61,7 @@ class AudioLoader(load.LoaderPlugin): ) activate_sound = current_sound == audio_node - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) cmds.sound( audio_node, @@ -94,7 +94,7 @@ class AudioLoader(load.LoaderPlugin): cmds.setAttr( container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string" ) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py b/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py index 9453c9c9c6..080d20b0a6 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_gpucache.py @@ -24,11 +24,10 @@ class GpuCacheLoader(load.LoaderPlugin): color = "orange" def load(self, context, name, namespace, data): - - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -75,8 +74,8 @@ class GpuCacheLoader(load.LoaderPlugin): loader=self.__class__.__name__) def update(self, container, context): - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) # Update the cache members = cmds.sets(container['objectName'], query=True) @@ -88,7 +87,7 @@ class GpuCacheLoader(load.LoaderPlugin): cmds.setAttr(cache + ".cacheFileName", path, type="string") cmds.setAttr(container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string") def switch(self, container, context): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image.py b/client/ayon_core/hosts/maya/plugins/load/load_image.py index 7b324986f0..d595aa2987 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image.py @@ -114,11 +114,10 @@ class FileNodeLoader(load.LoaderPlugin): ] def load(self, context, name, namespace, data): - - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -147,7 +146,7 @@ class FileNodeLoader(load.LoaderPlugin): ) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] members = cmds.sets(container['objectName'], query=True) file_node = cmds.ls(members, type="file")[0] @@ -157,7 +156,7 @@ class FileNodeLoader(load.LoaderPlugin): # Update representation cmds.setAttr( container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string" ) @@ -223,15 +222,18 @@ class FileNodeLoader(load.LoaderPlugin): def _is_sequence(self, context): """Check whether frameStart and frameEnd are not the same.""" - version = context.get("version", {}) - representation = context.get("representation", {}) + version = context["version"] + representation = context["representation"] - for doc in [representation, version]: + # TODO this is invalid logic, it should be based only on + # representation entity + for entity in [representation, version]: # Frame range can be set on version or representation. # When set on representation it overrides version data. - data = doc.get("data", {}) - start = data.get("frameStartHandle", data.get("frameStart", None)) - end = data.get("frameEndHandle", data.get("frameEnd", None)) + attributes = entity["attrib"] + data = entity["data"] + start = data.get("frameStartHandle", attributes.get("frameStart")) + end = data.get("frameEndHandle", attributes.get("frameEnd")) if start is None or end is None: continue @@ -301,7 +303,7 @@ class FileNodeLoader(load.LoaderPlugin): context = copy.deepcopy(context) representation = context["representation"] - template = representation.get("data", {}).get("template") + template = representation.get("attrib", {}).get("template") if not template: # No template to find token locations for return get_representation_path_from_context(context) @@ -327,7 +329,7 @@ class FileNodeLoader(load.LoaderPlugin): has_tokens = True # Replace with our custom template that has the tokens set - representation["data"]["template"] = template + representation["attrib"]["template"] = template path = get_representation_path_from_context(context) if has_tokens: diff --git a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py index 2366f6edd7..71ae892150 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_image_plane.py @@ -1,14 +1,8 @@ from qtpy import QtWidgets, QtCore -from ayon_core.client import ( - get_asset_by_id, - get_subset_by_id, - get_version_by_id, -) from ayon_core.pipeline import ( load, get_representation_path, - get_current_project_name, ) from ayon_core.hosts.maya.api.pipeline import containerise from ayon_core.hosts.maya.api.lib import ( @@ -102,10 +96,10 @@ class ImagePlaneLoader(load.LoaderPlugin): def load(self, context, name, namespace, data, options=None): image_plane_depth = 1000 - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -206,25 +200,25 @@ class ImagePlaneLoader(load.LoaderPlugin): ) def update(self, container, context): - asset_doc = context["asset"] - repre_doc = context["representation"] + folder_entity = context["folder"] + repre_entity = context["representation"] members = get_container_members(container) image_planes = cmds.ls(members, type="imagePlane") assert image_planes, "Image plane not found." image_plane_shape = image_planes[0] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) cmds.setAttr("{}.imageName".format(image_plane_shape), path, type="string") cmds.setAttr("{}.representation".format(container["objectName"]), - str(repre_doc["_id"]), + repre_entity["id"], type="string") # Set frame range. - start_frame = asset_doc["data"]["frameStart"] - end_frame = asset_doc["data"]["frameEnd"] + start_frame = folder_entity["attrib"]["frameStart"] + end_frame = folder_entity["attrib"]["frameEnd"] for attr, value in { "frameOffset": 0, diff --git a/client/ayon_core/hosts/maya/plugins/load/load_look.py b/client/ayon_core/hosts/maya/plugins/load/load_look.py index fb5be14aa1..9226c7b16f 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_look.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_look.py @@ -4,12 +4,9 @@ import json from collections import defaultdict from qtpy import QtWidgets +from ayon_api import get_representation_by_name -from ayon_core.client import get_representation_by_name -from ayon_core.pipeline import ( - get_current_project_name, - get_representation_path, -) +from ayon_core.pipeline import get_representation_path import ayon_core.hosts.maya.api.plugin from ayon_core.hosts.maya.api import lib from ayon_core.hosts.maya.api.lib import get_reference_node @@ -78,10 +75,10 @@ class LookLoader(ayon_core.hosts.maya.api.plugin.ReferenceLoader): shader_nodes = cmds.ls(members, type='shadingEngine') nodes = set(self._get_nodes_with_shader(shader_nodes)) - version_doc = context["version"] - project_name = get_current_project_name() + version_id = context["version"]["id"] + project_name = context["project"]["name"] json_representation = get_representation_by_name( - project_name, "json", version_doc["_id"] + project_name, "json", version_id ) # Load relationships diff --git a/client/ayon_core/hosts/maya/plugins/load/load_maya_usd.py b/client/ayon_core/hosts/maya/plugins/load/load_maya_usd.py index cb9fde7b33..0a8adaf87f 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_maya_usd.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_maya_usd.py @@ -25,10 +25,10 @@ class MayaUsdLoader(load.LoaderPlugin): color = "orange" def load(self, context, name=None, namespace=None, options=None): - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -78,13 +78,13 @@ class MayaUsdLoader(load.LoaderPlugin): members = cmds.sets(node, query=True) or [] shapes = cmds.ls(members, type="mayaUsdProxyShape") - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) for shape in shapes: cmds.setAttr("{}.filePath".format(shape), path, type="string") cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") def switch(self, container, context): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd.py b/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd.py index 64e6048c31..aab87fc546 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd.py @@ -3,6 +3,8 @@ import maya.cmds as cmds from maya import mel import os +from ayon_api import get_representation_by_id + from ayon_core.pipeline import ( load, get_representation_path @@ -13,7 +15,6 @@ from ayon_core.hosts.maya.api.lib import ( unique_namespace ) from ayon_core.hosts.maya.api.pipeline import containerise -from ayon_core.client import get_representation_by_id class MultiverseUsdLoader(load.LoaderPlugin): @@ -29,10 +30,10 @@ class MultiverseUsdLoader(load.LoaderPlugin): color = "orange" def load(self, context, name=None, namespace=None, options=None): - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -71,12 +72,12 @@ class MultiverseUsdLoader(load.LoaderPlugin): assert shapes, "Cannot find mvUsdCompoundShape in container" project_name = context["project"]["name"] - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) prev_representation_id = cmds.getAttr("{}.representation".format(node)) prev_representation = get_representation_by_id(project_name, prev_representation_id) - prev_path = os.path.normpath(prev_representation["data"]["path"]) + prev_path = os.path.normpath(prev_representation["attrib"]["path"]) # Make sure we can load the plugin cmds.loadPlugin("MultiverseForMaya", quiet=True) @@ -96,7 +97,7 @@ class MultiverseUsdLoader(load.LoaderPlugin): multiverse.SetUsdCompoundAssetPaths(shape, asset_paths) cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") mel.eval('refreshEditorTemplates;') diff --git a/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd_over.py b/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd_over.py index 6de03fe306..70069d3ae6 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd_over.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_multiverse_usd_over.py @@ -4,6 +4,7 @@ from maya import mel import os import qargparse +from ayon_api import get_representation_by_id from ayon_core.pipeline import ( load, @@ -13,7 +14,6 @@ from ayon_core.hosts.maya.api.lib import ( maintained_selection ) from ayon_core.hosts.maya.api.pipeline import containerise -from ayon_core.client import get_representation_by_id class MultiverseUsdOverLoader(load.LoaderPlugin): @@ -89,13 +89,13 @@ class MultiverseUsdOverLoader(load.LoaderPlugin): assert mvShape, "Missing mv source" project_name = context["project"]["name"] - repre_doc = context["representation"] + repre_entity = context["representation"] prev_representation_id = cmds.getAttr("{}.representation".format(node)) prev_representation = get_representation_by_id(project_name, prev_representation_id) - prev_path = os.path.normpath(prev_representation["data"]["path"]) + prev_path = os.path.normpath(prev_representation["attrib"]["path"]) - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) for shape in shapes: asset_paths = multiverse.GetUsdCompoundAssetPaths(shape) @@ -108,7 +108,7 @@ class MultiverseUsdOverLoader(load.LoaderPlugin): multiverse.SetUsdCompoundAssetPaths(shape, asset_paths) cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") mel.eval('refreshEditorTemplates;') diff --git a/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py b/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py index feb63ae4be..a0ed7cd6e7 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_redshift_proxy.py @@ -37,10 +37,10 @@ class RedshiftProxyLoader(load.LoaderPlugin): except ValueError: product_type = "redshiftproxy" - asset_name = context['asset']["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset_name + "_", - prefix="_" if asset_name[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -83,8 +83,8 @@ class RedshiftProxyLoader(load.LoaderPlugin): members = cmds.sets(node, query=True) or [] rs_meshes = cmds.ls(members, type="RedshiftProxyMesh") assert rs_meshes, "Cannot find RedshiftProxyMesh in container" - repre_doc = context["representation"] - filename = get_representation_path(repre_doc) + repre_entity = context["representation"] + filename = get_representation_path(repre_entity) for rs_mesh in rs_meshes: cmds.setAttr("{}.fileName".format(rs_mesh), @@ -93,7 +93,7 @@ class RedshiftProxyLoader(load.LoaderPlugin): # Update metadata cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") def remove(self, container): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_rendersetup.py b/client/ayon_core/hosts/maya/plugins/load/load_rendersetup.py index 58f161afc1..0f01a65539 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_rendersetup.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_rendersetup.py @@ -37,10 +37,10 @@ class RenderSetupLoader(load.LoaderPlugin): # from ayon_core.hosts.maya.api.lib import namespaced - asset = context['asset']['name'] + folder_name = context["folder"]["name"] namespace = namespace or lib.unique_namespace( - asset + "_", - prefix="_" if asset[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) path = self.filepath_from_context(context) @@ -91,8 +91,8 @@ class RenderSetupLoader(load.LoaderPlugin): "Render setup setting will be overwritten by new version. All " "setting specified by user not included in loaded version " "will be lost.") - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) with open(path, "r") as file: try: renderSetup.instance().decode( @@ -104,7 +104,7 @@ class RenderSetupLoader(load.LoaderPlugin): # Update metadata node = container["objectName"] cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") self.log.info("... updated") diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py index 3eec09eb7d..05dac0e260 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_arnold.py @@ -37,11 +37,10 @@ class LoadVDBtoArnold(load.LoaderPlugin): self.log.error("Encountered exception:\n%s" % exc) return - asset = context['asset'] - asset_name = asset["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset_name + "_", - prefix="_" if asset_name[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -85,9 +84,9 @@ class LoadVDBtoArnold(load.LoaderPlugin): from maya import cmds - repre_doc = context["representation"] + repre_entity = context["representation"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) @@ -95,11 +94,11 @@ class LoadVDBtoArnold(load.LoaderPlugin): assert len(grid_nodes) == 1, "This is a bug" # Update the VRayVolumeGrid - self._set_path(grid_nodes[0], path=path, representation=repre_doc) + self._set_path(grid_nodes[0], path=path, representation=repre_entity) # Update container representation cmds.setAttr(container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string") def switch(self, container, context): @@ -125,14 +124,14 @@ class LoadVDBtoArnold(load.LoaderPlugin): @staticmethod def _set_path(grid_node, path, - representation): + repre_entity): """Apply the settings for the VDB path to the aiVolume node""" from maya import cmds if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) - is_sequence = bool(representation["context"].get("frame")) + is_sequence = "frame" in repre_entity["context"] cmds.setAttr(grid_node + ".useFrameExtension", is_sequence) # Set file path diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py index f58d9e5565..b6cd513d9e 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_redshift.py @@ -55,12 +55,10 @@ class LoadVDBtoRedShift(load.LoaderPlugin): "set the render engine to '%s'" % compatible) - asset = context['asset'] - - asset_name = asset["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset_name + "_", - prefix="_" if asset_name[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -98,8 +96,8 @@ class LoadVDBtoRedShift(load.LoaderPlugin): def update(self, container, context): from maya import cmds - repre_doc = context["representation"] - path = get_representation_path(repre_doc) + repre_entity = context["representation"] + path = get_representation_path(repre_entity) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) @@ -107,11 +105,11 @@ class LoadVDBtoRedShift(load.LoaderPlugin): assert len(grid_nodes) == 1, "This is a bug" # Update the VRayVolumeGrid - self._set_path(grid_nodes[0], path=path, representation=repre_doc) + self._set_path(grid_nodes[0], path=path, representation=repre_entity) # Update container representation cmds.setAttr(container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string") def remove(self, container): @@ -143,7 +141,7 @@ class LoadVDBtoRedShift(load.LoaderPlugin): if not os.path.exists(path): raise RuntimeError("Path does not exist: %s" % path) - is_sequence = bool(representation["context"].get("frame")) + is_sequence = "frame" in representation["context"] cmds.setAttr(grid_node + ".useFrameExtension", is_sequence) # Set file path diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py index 6e82bbd5e2..8b5923bdbb 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vdb_to_vray.py @@ -116,11 +116,10 @@ class LoadVDBtoVRay(load.LoaderPlugin): "See Preferences > Display > Viewport 2.0 to " "set the render engine to '%s'" % compatible) - asset = context['asset'] - asset_name = asset["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset_name + "_", - prefix="_" if asset_name[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -255,9 +254,9 @@ class LoadVDBtoVRay(load.LoaderPlugin): type="string") def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) # Find VRayVolumeGrid members = cmds.sets(container['objectName'], query=True) @@ -270,7 +269,7 @@ class LoadVDBtoVRay(load.LoaderPlugin): # Update container representation cmds.setAttr(container["objectName"] + ".representation", - str(repre_doc["_id"]), + repre_entity["id"], type="string") def switch(self, container, context): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py b/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py index 86ea8004ba..fe1f8425d8 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vrayproxy.py @@ -7,13 +7,12 @@ loader will use them instead of native vray vrmesh format. """ import os +from ayon_api import get_representation_by_name import maya.cmds as cmds -from ayon_core.client import get_representation_by_name from ayon_core.settings import get_project_settings from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.maya.api.lib import ( @@ -54,14 +53,16 @@ class VRayProxyLoader(load.LoaderPlugin): product_type = "vrayproxy" # get all representations for this version - filename = self._get_abc(context["version"]["_id"]) + filename = self._get_abc( + context["project"]["name"], context["version"]["id"] + ) if not filename: filename = self.filepath_from_context(context) - asset_name = context['asset']["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset_name + "_", - prefix="_" if asset_name[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -107,11 +108,12 @@ class VRayProxyLoader(load.LoaderPlugin): assert vraymeshes, "Cannot find VRayMesh in container" # get all representations for this version - repre_doc = context["representation"] - filename = ( - self._get_abc(repre_doc["parent"]) - or get_representation_path(repre_doc) + repre_entity = context["representation"] + filename = self._get_abc( + context["project"]["name"], context["version"]["id"] ) + if not filename: + filename = get_representation_path(repre_entity) for vray_mesh in vraymeshes: cmds.setAttr("{}.fileName".format(vray_mesh), @@ -120,7 +122,7 @@ class VRayProxyLoader(load.LoaderPlugin): # Update metadata cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") def remove(self, container): @@ -170,7 +172,7 @@ class VRayProxyLoader(load.LoaderPlugin): return [parent, proxy], parent - def _get_abc(self, version_id): + def _get_abc(self, project_name, version_id): # type: (str) -> str """Get abc representation file path if present. @@ -178,6 +180,7 @@ class VRayProxyLoader(load.LoaderPlugin): vray proxy, get is file path. Args: + project_name (str): Project name. version_id (str): Version hash id. Returns: @@ -187,8 +190,9 @@ class VRayProxyLoader(load.LoaderPlugin): """ self.log.debug( "Looking for abc in published representations of this version.") - project_name = get_current_project_name() - abc_rep = get_representation_by_name(project_name, "abc", version_id) + abc_rep = ayon_api.get_representation_by_name( + project_name, "abc", version_id + ) if abc_rep: self.log.debug("Found, we'll link alembic to vray proxy.") file_name = get_representation_path(abc_rep) diff --git a/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py b/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py index a5bfd9dcc3..3d21464edc 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_vrayscene.py @@ -31,10 +31,10 @@ class VRaySceneLoader(load.LoaderPlugin): except ValueError: product_type = "vrayscene_layer" - asset_name = context['asset']["name"] + folder_name = context["folder"]["name"] namespace = namespace or unique_namespace( - asset_name + "_", - prefix="_" if asset_name[0].isdigit() else "", + folder_name + "_", + prefix="_" if folder_name[0].isdigit() else "", suffix="_", ) @@ -80,8 +80,8 @@ class VRaySceneLoader(load.LoaderPlugin): vraymeshes = cmds.ls(members, type="VRayScene") assert vraymeshes, "Cannot find VRayScene in container" - repre_doc = context["representation"] - filename = get_representation_path(repre_doc) + repre_entity = context["representation"] + filename = get_representation_path(repre_entity) for vray_mesh in vraymeshes: cmds.setAttr("{}.FilePath".format(vray_mesh), @@ -90,7 +90,7 @@ class VRaySceneLoader(load.LoaderPlugin): # Update metadata cmds.setAttr("{}.representation".format(node), - str(repre_doc["_id"]), + repre_entity["id"], type="string") def remove(self, container): diff --git a/client/ayon_core/hosts/maya/plugins/load/load_xgen.py b/client/ayon_core/hosts/maya/plugins/load/load_xgen.py index fdac62a250..58a6d86292 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_xgen.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_xgen.py @@ -147,8 +147,8 @@ class XgenLoader(ayon_core.hosts.maya.api.plugin.ReferenceLoader): self.set_palette_attributes(xgen_palette, xgen_file, xgd_file) - repre_doc = context["representation"] - maya_file = get_representation_path(repre_doc) + repre_entity = context["representation"] + maya_file = get_representation_path(repre_entity) _, extension = os.path.splitext(maya_file) new_xgen_file = maya_file.replace(extension, ".xgen") data_path = "" diff --git a/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py b/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py index 1c6423a8d7..18a094c29d 100644 --- a/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py +++ b/client/ayon_core/hosts/maya/plugins/load/load_yeti_cache.py @@ -62,9 +62,9 @@ class YetiCacheLoader(load.LoaderPlugin): product_type = "yeticache" # Build namespace - asset = context["asset"] + folder_name = context["folder"]["name"] if namespace is None: - namespace = self.create_namespace(asset["name"]) + namespace = self.create_namespace(folder_name) # Ensure Yeti is loaded if not cmds.pluginInfo("pgYetiMaya", query=True, loaded=True): @@ -123,11 +123,11 @@ class YetiCacheLoader(load.LoaderPlugin): cmds.namespace(removeNamespace=namespace, deleteNamespaceContent=True) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] namespace = container["namespace"] container_node = container["objectName"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) settings = self.read_settings(path) # Collect scene information of asset @@ -216,22 +216,22 @@ class YetiCacheLoader(load.LoaderPlugin): set_attribute(attr, value, yeti_node) cmds.setAttr("{}.representation".format(container_node), - str(repre_doc["_id"]), + repre_entity["id"], typ="string") def switch(self, container, context): self.update(container, context) # helper functions - def create_namespace(self, asset): + def create_namespace(self, folder_name): """Create a unique namespace Args: asset (dict): asset information """ - asset_name = "{}_".format(asset) - prefix = "_" if asset_name[0].isdigit()else "" + asset_name = "{}_".format(folder_name) + prefix = "_" if asset_name[0].isdigit() else "" namespace = lib.unique_namespace( asset_name, prefix=prefix, diff --git a/client/ayon_core/hosts/maya/plugins/publish/collect_review.py b/client/ayon_core/hosts/maya/plugins/publish/collect_review.py index 58d02294c5..4e35b3bcc2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/collect_review.py +++ b/client/ayon_core/hosts/maya/plugins/publish/collect_review.py @@ -1,8 +1,8 @@ from maya import cmds, mel +import ayon_api import pyblish.api -from ayon_core.client import get_subset_by_name from ayon_core.pipeline import KnownPublishError from ayon_core.hosts.maya.api import lib @@ -67,7 +67,7 @@ class CollectReview(pyblish.api.InstancePlugin): reviewable_product = reviewable_products[0] self.log.debug( - "Subset attached to review: {}".format(reviewable_product) + "Product attached to review: {}".format(reviewable_product) ) # Find the relevant publishing instance in the current context @@ -117,16 +117,16 @@ class CollectReview(pyblish.api.InstancePlugin): else: project_name = instance.context.data["projectName"] - asset_doc = instance.context.data['assetEntity'] + folder_entity = instance.context.data["folderEntity"] task = instance.context.data["task"] legacy_product_name = task + 'Review' - subset_doc = get_subset_by_name( + product_entity = ayon_api.get_product_by_name( project_name, legacy_product_name, - asset_doc["_id"], - fields=["_id"] + folder_entity["id"], + fields={"id"} ) - if subset_doc: + if product_entity: self.log.debug("Existing products found, keep legacy name.") instance.data["productName"] = legacy_product_name diff --git a/client/ayon_core/hosts/maya/plugins/publish/determine_future_version.py b/client/ayon_core/hosts/maya/plugins/publish/determine_future_version.py index 47fb4f03fe..5b597f2707 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/determine_future_version.py +++ b/client/ayon_core/hosts/maya/plugins/publish/determine_future_version.py @@ -5,7 +5,7 @@ class DetermineFutureVersion(pyblish.api.InstancePlugin): """ This will determine version of product if we want render to be attached to. """ - label = "Determine Subset Version" + label = "Determine Product Version" order = pyblish.api.IntegratorOrder hosts = ["maya"] families = ["renderlayer"] diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_layout.py b/client/ayon_core/hosts/maya/plugins/publish/extract_layout.py index 441610b749..b025a1605a 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_layout.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_layout.py @@ -4,8 +4,8 @@ import json from maya import cmds from maya.api import OpenMaya as om +from ayon_api import get_representation_by_id -from ayon_core.client import get_representation_by_id from ayon_core.pipeline import publish @@ -44,6 +44,8 @@ class ExtractLayout(publish.Extractor): grp_loaded_ass = instance.data.get("groupLoadedAssets", False) if grp_loaded_ass: asset_list = cmds.listRelatives(asset, children=True) + # WARNING This does override 'asset' variable from parent loop + # is it correct? for asset in asset_list: grp_name = asset.split(':')[0] else: @@ -61,13 +63,18 @@ class ExtractLayout(publish.Extractor): representation = get_representation_by_id( project_name, representation_id, - fields=["parent", "context.family"] + fields={"versionId", "context"} ) self.log.debug(representation) - version_id = representation.get("parent") - product_type = representation.get("context").get("family") + version_id = representation["versionId"] + # TODO use product entity to get product type rather than + # data in representation 'context' + repre_context = representation["context"] + product_type = repre_context.get("product", {}).get("type") + if not product_type: + product_type = repre_context.get("family") json_element = { "product_type": product_type, diff --git a/client/ayon_core/hosts/maya/plugins/publish/extract_look.py b/client/ayon_core/hosts/maya/plugins/publish/extract_look.py index 469608100d..fa74dd18d3 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/extract_look.py +++ b/client/ayon_core/hosts/maya/plugins/publish/extract_look.py @@ -1,16 +1,17 @@ # -*- coding: utf-8 -*- """Maya look extractor.""" +import os import sys -from abc import ABCMeta, abstractmethod -from collections import OrderedDict import contextlib import json import logging -import os import tempfile +import platform +from abc import ABCMeta, abstractmethod +from collections import OrderedDict + import six import attr - import pyblish.api from maya import cmds # noqa diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_frame_range.py b/client/ayon_core/hosts/maya/plugins/publish/validate_frame_range.py index 5c5b691f9d..5736e726e9 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_frame_range.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_frame_range.py @@ -80,7 +80,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, return if (inst_start != frame_start_handle): errors.append("Instance start frame [ {} ] doesn't " - "match the one set on asset [ {} ]: " + "match the one set on folder [ {} ]: " "{}/{}/{}/{} (handle/start/end/handle)".format( inst_start, frame_start_handle, @@ -89,7 +89,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, if (inst_end != frame_end_handle): errors.append("Instance end frame [ {} ] doesn't " - "match the one set on asset [ {} ]: " + "match the one set on folder [ {} ]: " "{}/{}/{}/{} (handle/start/end/handle)".format( inst_end, frame_end_handle, @@ -105,7 +105,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, for label, values in checks.items(): if values[0] != values[1]: errors.append( - "{} on instance ({}) does not match with the asset " + "{} on instance ({}) does not match with the folder " "({}).".format(label.title(), values[1], values[0]) ) @@ -119,7 +119,7 @@ class ValidateFrameRange(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): """ - Repair instance container to match asset data. + Repair instance container to match folder data. """ if "renderlayer" in instance.data.get("families"): diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_instance_in_context.py b/client/ayon_core/hosts/maya/plugins/publish/validate_instance_in_context.py index 43b4f06e3f..c5a3b1659d 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_instance_in_context.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_instance_in_context.py @@ -37,22 +37,22 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin, if not self.is_active(instance.data): return - asset = instance.data.get("folderPath") - context_asset = self.get_context_asset(instance) - if asset != context_asset: + folder_path = instance.data.get("folderPath") + context_folder_path = self.get_context_folder_path(instance) + if folder_path != context_folder_path: raise PublishValidationError( message=( - "Instance '{}' publishes to different asset than current " - "context: {}. Current context: {}".format( - instance.name, asset, context_asset + "Instance '{}' publishes to different folder than current" + " context: {}. Current context: {}".format( + instance.name, folder_path, context_folder_path ) ), description=( - "## Publishing to a different asset\n" + "## Publishing to a different folder\n" "There are publish instances present which are publishing " - "into a different asset than your current context.\n\n" + "into a different folder than your current context.\n\n" "Usually this is not what you want but there can be cases " - "where you might want to publish into another asset or " + "where you might want to publish into another folder or " "shot. If that's the case you can disable the validation " "on the instance to ignore it." ) @@ -64,14 +64,14 @@ class ValidateInstanceInContext(pyblish.api.InstancePlugin, @classmethod def repair(cls, instance): - context_asset = cls.get_context_asset(instance) + context_folder_path = cls.get_context_folder_path(instance) instance_node = instance.data["instance_node"] cmds.setAttr( "{}.folderPath".format(instance_node), - context_asset, + context_folder_path, type="string" ) @staticmethod - def get_context_asset(instance): + def get_context_folder_path(instance): return instance.context.data["folderPath"] diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py b/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py index eca27d95da..1c00d86868 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_maya_units.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import pyblish.api import ayon_core.hosts.maya.api.lib as mayalib -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.pipeline.publish import ( RepairContextAction, ValidateSceneOrder, @@ -59,8 +59,8 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): fps = context.data.get('fps') - asset_doc = context.data["assetEntity"] - asset_fps = mayalib.convert_to_maya_fps(asset_doc["data"]["fps"]) + folder_attributes = context.data["folderEntity"]["attrib"] + folder_fps = mayalib.convert_to_maya_fps(folder_attributes["fps"]) self.log.info('Units (linear): {0}'.format(linearunits)) self.log.info('Units (angular): {0}'.format(angularunits)) @@ -91,10 +91,10 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): "current_value": angularunits }) - if self.validate_fps and fps and fps != asset_fps: + if self.validate_fps and fps and fps != folder_fps: invalid.append({ "setting": "FPS", - "required_value": asset_fps, + "required_value": folder_fps, "current_value": fps }) @@ -127,7 +127,6 @@ class ValidateMayaUnits(pyblish.api.ContextPlugin): cls.log.debug(current_linear) cls.log.info("Setting time unit to match project") - # TODO replace query with using 'context.data["assetEntity"]' - asset_doc = get_current_project_asset() - asset_fps = asset_doc["data"]["fps"] - mayalib.set_scene_fps(asset_fps) + # TODO replace query with using 'context.data["folderEntity"]' + folder_entity = get_current_project_folder() + mayalib.set_scene_fps(folder_entity["attrib"]["fps"]) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py index de86ffe575..65a779f3f0 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_in_database.py @@ -1,14 +1,14 @@ import pyblish.api +import ayon_api import ayon_core.hosts.maya.api.action -from ayon_core.client import get_assets from ayon_core.hosts.maya.api import lib from ayon_core.pipeline.publish import ( PublishValidationError, ValidatePipelineOrder) class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): - """Validate if the CB Id is related to an asset in the database + """Validate if the CB Id is related to an folder in the database All nodes with the `cbId` attribute will be validated to ensure that the loaded asset in the scene is related to the current project. @@ -30,7 +30,7 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): invalid = self.get_invalid(instance) if invalid: raise PublishValidationError( - ("Found asset IDs which are not related to " + ("Found folder ids which are not related to " "current project in instance: `{}`").format(instance.name)) @classmethod @@ -44,10 +44,10 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): # check ids against database ids project_name = instance.context.data["projectName"] - asset_docs = get_assets(project_name, fields=["_id"]) - db_asset_ids = { - str(asset_doc["_id"]) - for asset_doc in asset_docs + folder_entities = ayon_api.get_folders(project_name, fields={"id"}) + folder_ids = { + folder_entity["id"] + for folder_entity in folder_entities } # Get all asset IDs @@ -58,9 +58,9 @@ class ValidateNodeIdsInDatabase(pyblish.api.InstancePlugin): if not cb_id: continue - asset_id = cb_id.split(":", 1)[0] - if asset_id not in db_asset_ids: - cls.log.error("`%s` has unassociated asset ID" % node) + folder_id = cb_id.split(":", 1)[0] + if folder_id not in folder_ids: + cls.log.error("`%s` has unassociated folder id" % node) invalid.append(node) return invalid diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py index b2db535fa6..606abee3d2 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_related.py @@ -8,7 +8,8 @@ from ayon_core.pipeline.publish import ( class ValidateNodeIDsRelated(pyblish.api.InstancePlugin, OptionalPyblishPluginMixin): - """Validate nodes have a related Colorbleed Id to the instance.data[asset] + """Validate nodes have a related Colorbleed Id to the + instance.data[folderPath] """ @@ -31,27 +32,28 @@ class ValidateNodeIDsRelated(pyblish.api.InstancePlugin, # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: - raise PublishValidationError( - ("Nodes IDs found that are not related to asset " - "'{}' : {}").format(instance.data['asset'], invalid)) + raise PublishValidationError(( + "Nodes IDs found that are not related to folder '{}' : {}" + ).format( + instance.data["folderPath"], invalid + )) @classmethod def get_invalid(cls, instance): """Return the member nodes that are invalid""" invalid = list() - asset_id = str(instance.data['assetEntity']["_id"]) + folder_id = instance.data["folderEntity"]["id"] # We do want to check the referenced nodes as we it might be # part of the end product for node in instance: - _id = lib.get_id(node) if not _id: continue - node_asset_id = _id.split(":", 1)[0] - if node_asset_id != asset_id: + node_folder_id = _id.split(":", 1)[0] + if node_folder_id != folder_id: invalid.append(node) return invalid diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py index eeede82caf..6c5cd26259 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_node_ids_unique.py @@ -32,10 +32,10 @@ class ValidateNodeIdsUnique(pyblish.api.InstancePlugin): # Ensure all nodes have a cbId invalid = self.get_invalid(instance) if invalid: - label = "Nodes found with non-unique asset IDs" + label = "Nodes found with non-unique folder ids" raise PublishValidationError( message="{}: {}".format(label, invalid), - title="Non-unique asset ids on nodes", + title="Non-unique folder ids on nodes", description="{}\n- {}".format(label, "\n- ".join(sorted(invalid))) ) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py b/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py index 900e5444a9..90f256ad45 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_renderlayer_aovs.py @@ -1,7 +1,7 @@ +import ayon_api import pyblish.api import ayon_core.hosts.maya.api.action -from ayon_core.client import get_subset_by_name from ayon_core.pipeline.publish import PublishValidationError @@ -37,11 +37,11 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): invalid = [] project_name = instance.context.data["projectName"] - asset_doc = instance.data["assetEntity"] + folder_entity = instance.data["folderEntity"] render_passes = instance.data.get("renderPasses", []) for render_pass in render_passes: is_valid = self.validate_product_registered( - project_name, asset_doc, render_pass + project_name, folder_entity, render_pass ) if not is_valid: invalid.append(render_pass) @@ -49,10 +49,10 @@ class ValidateRenderLayerAOVs(pyblish.api.InstancePlugin): return invalid def validate_product_registered( - self, project_name, asset_doc, product_name + self, project_name, folder_entity, product_name ): - """Check if product is registered in the database under the asset""" + """Check if product is registered in the database under the folder""" - return get_subset_by_name( - project_name, product_name, asset_doc["_id"], fields=["_id"] + return ayon_api.get_product_by_name( + project_name, product_name, folder_entity["id"], fields={"id"} ) diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_resolution.py b/client/ayon_core/hosts/maya/plugins/publish/validate_resolution.py index ff552f566d..1e5a9a944c 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_resolution.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_resolution.py @@ -37,7 +37,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, @classmethod def get_invalid_resolution(cls, instance): - width, height, pixelAspect = cls.get_db_resolution(instance) + width, height, pixelAspect = cls.get_folder_resolution(instance) current_renderer = instance.data["renderer"] layer = instance.data["renderlayer"] invalid = False @@ -68,7 +68,7 @@ class ValidateResolution(pyblish.api.InstancePlugin, if current_width != width or current_height != height: cls.log.error( "Render resolution {}x{} does not match " - "asset resolution {}x{}".format( + "folder resolution {}x{}".format( current_width, current_height, width, height )) @@ -76,26 +76,24 @@ class ValidateResolution(pyblish.api.InstancePlugin, if current_pixelAspect != pixelAspect: cls.log.error( "Render pixel aspect {} does not match " - "asset pixel aspect {}".format( + "folder pixel aspect {}".format( current_pixelAspect, pixelAspect )) invalid = True return invalid @classmethod - def get_db_resolution(cls, instance): - asset_doc = instance.data["assetEntity"] - project_doc = instance.context.data["projectEntity"] - for data in [asset_doc["data"], project_doc["data"]]: - if ( - "resolutionWidth" in data and - "resolutionHeight" in data and - "pixelAspect" in data - ): - width = data["resolutionWidth"] - height = data["resolutionHeight"] - pixelAspect = data["pixelAspect"] - return int(width), int(height), float(pixelAspect) + def get_folder_resolution(cls, instance): + folder_attributes = instance.data["folderEntity"]["attrib"] + if ( + "resolutionWidth" in folder_attributes + and "resolutionHeight" in folder_attributes + and "pixelAspect" in folder_attributes + ): + width = folder_attributes["resolutionWidth"] + height = folder_attributes["resolutionHeight"] + pixelAspect = folder_attributes["pixelAspect"] + return int(width), int(height), float(pixelAspect) # Defaults if not found in asset document or project document return 1920, 1080, 1.0 diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_shader_name.py b/client/ayon_core/hosts/maya/plugins/publish/validate_shader_name.py index 86ca0ca400..09c17202c5 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_shader_name.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_shader_name.py @@ -51,7 +51,7 @@ class ValidateShaderName(pyblish.api.InstancePlugin, descendants = cmds.ls(descendants, noIntermediate=True, long=True) shapes = cmds.ls(descendants, type=["nurbsSurface", "mesh"], long=True) - asset_name = instance.data.get("folderPath") + folder_path = instance.data.get("folderPath") # Check the number of connected shadingEngines per shape regex_compile = re.compile(cls.regex) @@ -71,12 +71,12 @@ class ValidateShaderName(pyblish.api.InstancePlugin, cls.log.error(error_message.format(shape, shader)) else: if 'asset' in regex_compile.groupindex: - if m.group('asset') != asset_name: + if m.group('asset') != folder_path: invalid.append(shape) message = error_message - message += " with missing asset name \"{2}\"" + message += " with missing folder path \"{2}\"" cls.log.error( - message.format(shape, shader, asset_name) + message.format(shape, shader, folder_path) ) return invalid diff --git a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py index c9860d27a0..88b0ff0e71 100644 --- a/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py +++ b/client/ayon_core/hosts/maya/plugins/publish/validate_unreal_staticmesh_naming.py @@ -100,8 +100,8 @@ class ValidateUnrealStaticMeshName(pyblish.api.InstancePlugin, cl_r = re.compile(regex_collision) - asset_name = instance.data["assetEntity"]["name"] - mesh_name = "{}{}".format(asset_name, + folder_name = instance.data["folderEntity"]["name"] + mesh_name = "{}{}".format(folder_name, instance.data.get("variant", [])) for obj in collision_set: diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/app.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/app.py index 0969666484..44d8dfda21 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/app.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/app.py @@ -2,10 +2,10 @@ import sys import time import logging +import ayon_api from qtpy import QtWidgets, QtCore from ayon_core import style -from ayon_core.client import get_last_version_by_subset_id from ayon_core.pipeline import get_current_project_name from ayon_core.tools.utils.lib import qt_app_context from ayon_core.hosts.maya.api.lib import ( @@ -227,9 +227,9 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): # (since assigning multiple to the same nodes makes no sense) assign_look = next( ( - subset_doc - for subset_doc in item["looks"] - if subset_doc["name"] in looks + product_entity + for product_entity in item["looks"] + if product_entity["name"] in looks ), None ) @@ -240,8 +240,8 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): continue # Get the latest version of this asset's look product - version = get_last_version_by_subset_id( - project_name, assign_look["_id"], fields=["_id"] + version_entity = ayon_api.get_last_version_by_product_id( + project_name, assign_look["id"], fields={"id"} ) product_name = assign_look["name"] @@ -283,7 +283,9 @@ class MayaLookAssignerWindow(QtWidgets.QWidget): # Assign look if nodes: - assign_look_by_version(nodes, version_id=version["_id"]) + assign_look_by_version( + nodes, version_id=version_entity["id"] + ) end = time.time() diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/arnold_standin.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/arnold_standin.py index 810e1fc88c..a20880dffc 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/arnold_standin.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/arnold_standin.py @@ -4,10 +4,11 @@ from collections import defaultdict import logging from maya import cmds +import ayon_api from ayon_core.pipeline import get_current_project_name -from ayon_core.client import get_last_version_by_subset_name from ayon_core.hosts.maya import api + from . import lib from .alembic import get_alembic_ids_cache from .usd import is_usd_lib_supported, get_usd_ids_cache @@ -136,33 +137,34 @@ def assign_look(standin, product_name): nodes_by_id = get_nodes_by_id(standin) - # Group by asset id so we run over the look per asset - node_ids_by_asset_id = defaultdict(set) + # Group by folder id so we run over the look per folder + node_ids_by_folder_id = defaultdict(set) for node_id in nodes_by_id: - asset_id = node_id.split(":", 1)[0] - node_ids_by_asset_id[asset_id].add(node_id) + folder_id = node_id.split(":", 1)[0] + node_ids_by_folder_id[folder_id].add(node_id) project_name = get_current_project_name() - for asset_id, node_ids in node_ids_by_asset_id.items(): + for folder_id, node_ids in node_ids_by_folder_id.items(): # Get latest look version - version = get_last_version_by_subset_name( + version_entity = ayon_api.get_last_version_by_product_name( project_name, - subset_name=product_name, - asset_id=asset_id, - fields=["_id"] + product_name, + folder_id, + fields={"id"} ) - if not version: + if not version_entity: log.info("Didn't find last version for product name {}".format( product_name )) continue + version_id = version_entity["id"] - relationships = lib.get_look_relationships(version["_id"]) - shader_nodes, container_node = lib.load_look(version["_id"]) + relationships = lib.get_look_relationships(version_id) + shader_nodes, container_node = lib.load_look(version_id) namespace = shader_nodes[0].split(":")[0] - # Get only the node ids and paths related to this asset + # Get only the node ids and paths related to this folder # And get the shader edits the look supplies asset_nodes_by_id = { node_id: nodes_by_id[node_id] for node_id in node_ids diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/commands.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/commands.py index 4375d38316..75c82164c2 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/commands.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/commands.py @@ -2,9 +2,9 @@ import os import logging from collections import defaultdict +import ayon_api import maya.cmds as cmds -from ayon_core.client import get_assets, get_asset_name_identifier from ayon_core.pipeline import ( remove_container, registered_host, @@ -62,7 +62,7 @@ def get_all_asset_nodes(): return cmds.ls(dag=True, noIntermediate=True, long=True) -def create_asset_id_hash(nodes): +def create_folder_id_hash(nodes): """Create a hash based on cbId attribute value Args: nodes (list): a list of nodes @@ -74,10 +74,10 @@ def create_asset_id_hash(nodes): for node in nodes: # iterate over content of reference node if cmds.nodeType(node) == "reference": - ref_hashes = create_asset_id_hash( + ref_hashes = create_folder_id_hash( list(set(cmds.referenceQuery(node, nodes=True, dp=True)))) - for asset_id, ref_nodes in ref_hashes.items(): - node_id_hash[asset_id] += ref_nodes + for folder_id, ref_nodes in ref_hashes.items(): + node_id_hash[folder_id] += ref_nodes elif cmds.pluginInfo('vrayformaya', query=True, loaded=True) and cmds.nodeType( node) == "VRayProxy": @@ -95,8 +95,8 @@ def create_asset_id_hash(nodes): if value is None: continue - asset_id = value.split(":")[0] - node_id_hash[asset_id].append(node) + folder_id = value.split(":")[0] + node_id_hash[folder_id].append(node) return dict(node_id_hash) @@ -104,10 +104,10 @@ def create_asset_id_hash(nodes): def create_items_from_nodes(nodes): """Create an item for the view based the container and content of it - It fetches the look document based on the asset ID found in the content. + It fetches the look document based on the folder id found in the content. The item will contain all important information for the tool to work. - If there is an asset ID which is not registered in the project's collection + If there is an folder id which is not registered in the project's collection it will log a warning message. Args: @@ -118,54 +118,55 @@ def create_items_from_nodes(nodes): """ - asset_view_items = [] + folder_view_items = [] - id_hashes = create_asset_id_hash(nodes) + id_hashes = create_folder_id_hash(nodes) if not id_hashes: log.warning("No id hashes") - return asset_view_items + return folder_view_items project_name = get_current_project_name() - asset_ids = set(id_hashes.keys()) - fields = {"_id", "name", "data.parents"} - asset_docs = get_assets(project_name, asset_ids, fields=fields) - asset_docs_by_id = { - str(asset_doc["_id"]): asset_doc - for asset_doc in asset_docs + folder_ids = set(id_hashes.keys()) + + folder_entities = ayon_api.get_folders( + project_name, folder_ids, fields={"id", "path"} + ) + folder_entities_by_id = { + folder_entity["id"]: folder_entity + for folder_entity in folder_entities } - for asset_id, id_nodes in id_hashes.items(): - asset_doc = asset_docs_by_id.get(asset_id) - # Skip if asset id is not found - if not asset_doc: + for folder_id, id_nodes in id_hashes.items(): + folder_entity = folder_entities_by_id.get(folder_id) + # Skip if folder id is not found + if not folder_entity: log.warning( - "Id found on {num} nodes for which no asset is found database," - " skipping '{asset_id}'".format( + "Id found on {num} nodes for which no folder is found database," + " skipping '{folder_id}'".format( num=len(nodes), - asset_id=asset_id + folder_id=folder_id ) ) continue - # Collect available look products for this asset - looks = lib.list_looks(project_name, asset_doc["_id"]) + # Collect available look products for this folder + looks = lib.list_looks(project_name, folder_entity["id"]) - # Collect namespaces the asset is found in + # Collect namespaces the folder is found in namespaces = set() for node in id_nodes: namespace = get_namespace_from_node(node) namespaces.add(namespace) - label = get_asset_name_identifier(asset_doc) - asset_view_items.append({ - "label": label, - "asset": asset_doc, + folder_view_items.append({ + "label": folder_entity["path"], + "folder_entity": folder_entity, "looks": looks, "namespaces": namespaces }) - return asset_view_items + return folder_view_items def remove_unused_looks(): diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/lib.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/lib.py index e3ebddb7d4..78fded12a9 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/lib.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/lib.py @@ -1,6 +1,8 @@ import json import logging +from ayon_api import get_representation_by_name + from ayon_core.pipeline import ( get_current_project_name, get_representation_path, @@ -9,7 +11,6 @@ from ayon_core.pipeline import ( loaders_from_representation, load_container ) -from ayon_core.client import get_representation_by_name from ayon_core.hosts.maya.api import lib @@ -29,7 +30,7 @@ def get_look_relationships(version_id): project_name = get_current_project_name() json_representation = get_representation_by_name( - project_name, representation_name="json", version_id=version_id + project_name, "json", version_id ) # Load relationships @@ -57,12 +58,12 @@ def load_look(version_id): project_name = get_current_project_name() # Get representations of shader file and relationships look_representation = get_representation_by_name( - project_name, representation_name="ma", version_id=version_id + project_name, "ma", version_id ) # See if representation is already loaded, if so reuse it. host = registered_host() - representation_id = str(look_representation['_id']) + representation_id = look_representation["id"] for container in host.ls(): if (container['loader'] == "LookLoader" and container['representation'] == representation_id): diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py index 4892125954..f5dad25ff0 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/models.py @@ -104,12 +104,12 @@ class LookModel(models.TreeModel): # Collect the assets per look name (from the items of the AssetModel) look_products = defaultdict(list) for asset_item in items: - asset = asset_item["asset"] + folder_entity = asset_item["folder_entity"] for look in asset_item["looks"]: - look_products[look["name"]].append(asset) + look_products[look["name"]].append(folder_entity) for product_name in sorted(look_products.keys()): - assets = look_products[product_name] + folder_entities = look_products[product_name] # Define nice label without "look" prefix for readability label = ( @@ -123,10 +123,10 @@ class LookModel(models.TreeModel): item_node["product"] = product_name # Amount of matching assets for this look - item_node["match"] = len(assets) + item_node["match"] = len(folder_entities) # Store the assets that have this product available - item_node["assets"] = assets + item_node["folder_entities"] = folder_entities self.add_child(item_node) diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py index df74dcd217..74cdbeb7d4 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/vray_proxies.py @@ -4,8 +4,8 @@ from collections import defaultdict import logging from maya import cmds +import ayon_api -from ayon_core.client import get_last_version_by_subset_name from ayon_core.pipeline import get_current_project_name import ayon_core.hosts.maya.lib as maya_lib from . import lib @@ -73,27 +73,28 @@ def vrayproxy_assign_look(vrayproxy, product_name="lookDefault"): # Group by asset id so we run over the look per asset node_ids_by_asset_id = defaultdict(set) for node_id in nodes_by_id: - asset_id = node_id.split(":", 1)[0] - node_ids_by_asset_id[asset_id].add(node_id) + folder_id = node_id.split(":", 1)[0] + node_ids_by_asset_id[folder_id].add(node_id) project_name = get_current_project_name() - for asset_id, node_ids in node_ids_by_asset_id.items(): + for folder_id, node_ids in node_ids_by_asset_id.items(): # Get latest look version - version = get_last_version_by_subset_name( + version_entity = ayon_api.get_last_version_by_product_name( project_name, - subset_name=product_name, - asset_id=asset_id, - fields=["_id"] + product_name, + folder_id, + fields={"id"} ) - if not version: + if not version_entity: print("Didn't find last version for product name {}".format( product_name )) continue + version_id = version_entity["id"] - relationships = lib.get_look_relationships(version["_id"]) - shadernodes, _ = lib.load_look(version["_id"]) + relationships = lib.get_look_relationships(version_id) + shadernodes, _ = lib.load_look(version_id) # Get only the node ids and paths related to this asset # And get the shader edits the look supplies diff --git a/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py b/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py index 234a1c149e..6cc5f156e3 100644 --- a/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py +++ b/client/ayon_core/hosts/maya/tools/mayalookassigner/widgets.py @@ -3,7 +3,6 @@ from collections import defaultdict from qtpy import QtWidgets, QtCore -from ayon_core.client import get_asset_name_identifier from ayon_core.tools.utils.models import TreeModel from ayon_core.tools.utils.lib import ( preserve_expanded_rows, @@ -110,7 +109,7 @@ class AssetOutliner(QtWidgets.QWidget): self.add_items(items) def get_nodes(self, selection=False): - """Find the nodes in the current scene per asset.""" + """Find the nodes in the current scene per folder.""" items = self.get_selected_items() @@ -119,26 +118,26 @@ class AssetOutliner(QtWidgets.QWidget): nodes = cmds.ls(dag=True, long=True) else: nodes = commands.get_selected_nodes() - id_nodes = commands.create_asset_id_hash(nodes) + id_nodes = commands.create_folder_id_hash(nodes) - # Collect the asset item entries per asset + # Collect the asset item entries per folder # and collect the namespaces we'd like to apply - assets = {} - asset_namespaces = defaultdict(set) + folder_items = {} + namespaces_by_folder_path = defaultdict(set) for item in items: - asset_id = str(item["asset"]["_id"]) - asset_name = get_asset_name_identifier(item["asset"]) - asset_namespaces[asset_name].add(item.get("namespace")) + folder_id = item["folder"]["id"] + folder_path = item["folder"]["path"] + namespaces_by_folder_path[folder_path].add(item.get("namespace")) - if asset_name in assets: + if folder_path in folder_items: continue - assets[asset_name] = item - assets[asset_name]["nodes"] = id_nodes.get(asset_id, []) + folder_items[folder_path] = item + folder_items[folder_path]["nodes"] = id_nodes.get(folder_id, []) # Filter nodes to namespace (if only namespaces were selected) - for asset_name in assets: - namespaces = asset_namespaces[asset_name] + for folder_path in folder_items: + namespaces = namespaces_by_folder_path[folder_path] # When None is present there should be no filtering if None in namespaces: @@ -146,12 +145,12 @@ class AssetOutliner(QtWidgets.QWidget): # Else only namespaces are selected and *not* the top entry so # we should filter to only those namespaces. - nodes = assets[asset_name]["nodes"] + nodes = folder_items[folder_path]["nodes"] nodes = [node for node in nodes if commands.get_namespace_from_node(node) in namespaces] - assets[asset_name]["nodes"] = nodes + folder_items[folder_path]["nodes"] = nodes - return assets + return folder_items def select_asset_from_items(self): """Select nodes from listed asset""" diff --git a/client/ayon_core/hosts/nuke/api/lib.py b/client/ayon_core/hosts/nuke/api/lib.py index e304b33dc7..8e39475f10 100644 --- a/client/ayon_core/hosts/nuke/api/lib.py +++ b/client/ayon_core/hosts/nuke/api/lib.py @@ -12,14 +12,7 @@ from collections import OrderedDict import nuke from qtpy import QtCore, QtWidgets - -from ayon_core.client import ( - get_project, - get_asset_by_name, - get_versions, - get_last_versions, - get_representations, -) +import ayon_api from ayon_core.host import HostDirmap from ayon_core.tools.utils import host_tools @@ -40,16 +33,15 @@ from ayon_core.settings import ( from ayon_core.addon import AddonsManager from ayon_core.pipeline.template_data import get_template_data_with_names from ayon_core.pipeline import ( - discover_legacy_creator_plugins, Anatomy, get_current_host_name, get_current_project_name, - get_current_asset_name, + get_current_folder_path, AYON_INSTANCE_ID, AVALON_INSTANCE_ID, ) from ayon_core.pipeline.context_tools import ( - get_custom_workfile_template_from_session + get_current_context_custom_workfile_template ) from ayon_core.pipeline.colorspace import get_imageio_config from ayon_core.pipeline.workfile import BuildWorkfile @@ -128,7 +120,7 @@ class Context: workfiles_tool_timer = None # Seems unused - _project_doc = None + _project_entity = None def get_main_window(): @@ -852,60 +844,62 @@ def check_inventory_versions(): project_name = get_current_project_name() # Find representations based on found containers - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_ids=repre_ids, - fields=["_id", "parent"] + fields={"id", "versionId"} ) # Store representations by id and collect version ids - repre_docs_by_id = {} + repre_entities_by_id = {} version_ids = set() - for repre_doc in repre_docs: + for repre_entity in repre_entities: # Use stringed representation id to match value in containers - repre_id = str(repre_doc["_id"]) - repre_docs_by_id[repre_id] = repre_doc - version_ids.add(repre_doc["parent"]) + repre_id = repre_entity["id"] + repre_entities_by_id[repre_id] = repre_entity + version_ids.add(repre_entity["versionId"]) - version_docs = get_versions( - project_name, version_ids, fields=["_id", "name", "parent"] + version_entities = ayon_api.get_versions( + project_name, + version_ids=version_ids, + fields={"id", "version", "productId"}, ) # Store versions by id and collect product ids - version_docs_by_id = {} + version_entities_by_id = {} product_ids = set() - for version_doc in version_docs: - version_docs_by_id[version_doc["_id"]] = version_doc - product_ids.add(version_doc["parent"]) + for version_entity in version_entities: + version_entities_by_id[version_entity["id"]] = version_entity + product_ids.add(version_entity["productId"]) # Query last versions based on product ids - last_versions_by_product_id = get_last_versions( - project_name, subset_ids=product_ids, fields=["_id", "parent"] + last_versions_by_product_id = ayon_api.get_last_versions( + project_name, product_ids=product_ids, fields={"id", "productId"} ) # Loop through collected container nodes and their representation ids for item in node_with_repre_id: # Some python versions of nuke can't unfold tuple in for loop node, repre_id = item - repre_doc = repre_docs_by_id.get(repre_id) + repre_entity = repre_entities_by_id.get(repre_id) # Failsafe for not finding the representation. - if not repre_doc: + if not repre_entity: log.warning(( "Could not find the representation on node \"{}\"" ).format(node.name())) continue - version_id = repre_doc["parent"] - version_doc = version_docs_by_id.get(version_id) - if not version_doc: + version_id = repre_entity["versionId"] + version_entity = version_entities_by_id.get(version_id) + if not version_entity: log.warning(( "Could not find the version on node \"{}\"" ).format(node.name())) continue # Get last version based on product id - product_id = version_doc["parent"] + product_id = version_entity["productId"] last_version = last_versions_by_product_id[product_id] # Check if last version is same as current version - if last_version["_id"] == version_doc["_id"]: + if last_version["id"] == version_entity["id"]: color_value = "0x4ecd25ff" else: color_value = "0xd84f20ff" @@ -1005,11 +999,11 @@ def format_anatomy(data): file = script_name() data["version"] = get_version_from_path(file) - asset_name = data["folderPath"] + folder_path = data["folderPath"] task_name = data["task"] host_name = get_current_host_name() context_data = get_template_data_with_names( - project_name, asset_name, task_name, host_name + project_name, folder_path, task_name, host_name ) data.update(context_data) data.update({ @@ -1453,17 +1447,19 @@ class WorkfileSettings(object): """ def __init__(self, root_node=None, nodes=None, **kwargs): - project_doc = kwargs.get("project") - if project_doc is None: + project_entity = kwargs.get("project") + if project_entity is None: project_name = get_current_project_name() - project_doc = get_project(project_name) + project_entity = ayon_api.get_project(project_name) else: - project_name = project_doc["name"] + project_name = project_entity["name"] - Context._project_doc = project_doc + Context._project_entity = project_entity self._project_name = project_name - self._asset = get_current_asset_name() - self._asset_entity = get_asset_by_name(project_name, self._asset) + self._folder_path = get_current_folder_path() + self._folder_entity = ayon_api.get_folder_by_path( + project_name, self._folder_path + ) self._root_node = root_node or nuke.root() self._nodes = self.get_nodes(nodes=nodes) @@ -1957,39 +1953,43 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. self.set_reads_colorspace(read_clrs_inputs) def reset_frame_range_handles(self): - """Set frame range to current asset""" + """Set frame range to current folder.""" - if "data" not in self._asset_entity: - msg = "Asset {} don't have set any 'data'".format(self._asset) + if "attrib" not in self._folder_entity: + msg = "Folder {} don't have set any 'attrib'".format( + self._folder_path + ) log.warning(msg) nuke.message(msg) return - asset_data = self._asset_entity["data"] + folder_attributes = self._folder_entity["attrib"] missing_cols = [] check_cols = ["fps", "frameStart", "frameEnd", "handleStart", "handleEnd"] for col in check_cols: - if col not in asset_data: + if col not in folder_attributes: missing_cols.append(col) if len(missing_cols) > 0: missing = ", ".join(missing_cols) - msg = "'{}' are not set for asset '{}'!".format( - missing, self._asset) + msg = "'{}' are not set for folder '{}'!".format( + missing, self._folder_path) log.warning(msg) nuke.message(msg) return # get handles values - handle_start = asset_data["handleStart"] - handle_end = asset_data["handleEnd"] + handle_start = folder_attributes["handleStart"] + handle_end = folder_attributes["handleEnd"] + frame_start = folder_attributes["frameStart"] + frame_end = folder_attributes["frameEnd"] - fps = float(asset_data["fps"]) - frame_start_handle = int(asset_data["frameStart"]) - handle_start - frame_end_handle = int(asset_data["frameEnd"]) + handle_end + fps = float(folder_attributes["fps"]) + frame_start_handle = frame_start - handle_start + frame_end_handle = frame_end + handle_end self._root_node["lock_range"].setValue(False) self._root_node["fps"].setValue(fps) @@ -2000,10 +2000,7 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. # update node graph so knobs are updated update_node_graph() - frame_range = '{0}-{1}'.format( - int(asset_data["frameStart"]), - int(asset_data["frameEnd"]) - ) + frame_range = '{0}-{1}'.format(frame_start, frame_end) for node in nuke.allNodes(filter="Viewer"): node['frame_range'].setValue(frame_range) @@ -2030,18 +2027,12 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. """Set resolution to project resolution.""" log.info("Resetting resolution") project_name = get_current_project_name() - asset_data = self._asset_entity["data"] + folder_attributes = self._folder_entity["attrib"] format_data = { - "width": int(asset_data.get( - 'resolutionWidth', - asset_data.get('resolution_width'))), - "height": int(asset_data.get( - 'resolutionHeight', - asset_data.get('resolution_height'))), - "pixel_aspect": asset_data.get( - 'pixelAspect', - asset_data.get('pixel_aspect', 1)), + "width": folder_attributes["resolutionWidth"], + "height": folder_attributes["resolutionHeight"], + "pixel_aspect": folder_attributes["pixelAspect"], "name": project_name } @@ -2108,7 +2099,11 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. from .utils import set_context_favorites work_dir = os.getenv("AYON_WORKDIR") - asset = get_current_asset_name() + # TODO validate functionality + # - does expect the structure is '{root}/{project}/{folder}' + # - this used asset name expecting it is unique in project + folder_path = get_current_folder_path() + folder_name = folder_path.split("/")[-1] favorite_items = OrderedDict() # project @@ -2120,13 +2115,13 @@ Reopening Nuke should synchronize these paths and resolve any discrepancies. # add to favorites favorite_items.update({"Project dir": project_dir.replace("\\", "/")}) - # asset - asset_root = os.path.normpath(work_dir.split( - asset)[0]) - # add asset name - asset_dir = os.path.join(asset_root, asset) + "/" + # folder + folder_root = os.path.normpath(work_dir.split( + folder_name)[0]) + # add folder name + folder_dir = os.path.join(folder_root, folder_name) + "/" # add to favorites - favorite_items.update({"Shot dir": asset_dir.replace("\\", "/")}) + favorite_items.update({"Shot dir": folder_dir.replace("\\", "/")}) # workdir favorite_items.update({"Work dir": work_dir.replace("\\", "/")}) @@ -2463,7 +2458,7 @@ def process_workfile_builder(): # generate first version in file not existing and feature is enabled if create_fv_on and not os.path.exists(last_workfile_path): # get custom template path if any - custom_template_path = get_custom_workfile_template_from_session( + custom_template_path = get_current_context_custom_workfile_template( project_settings=project_settings ) diff --git a/client/ayon_core/hosts/nuke/api/pipeline.py b/client/ayon_core/hosts/nuke/api/pipeline.py index 582df952d3..a8876f6aa7 100644 --- a/client/ayon_core/hosts/nuke/api/pipeline.py +++ b/client/ayon_core/hosts/nuke/api/pipeline.py @@ -21,7 +21,7 @@ from ayon_core.pipeline import ( AYON_INSTANCE_ID, AVALON_INSTANCE_ID, AVALON_CONTAINER_ID, - get_current_asset_name, + get_current_folder_path, get_current_task_name, registered_host, ) @@ -224,7 +224,7 @@ def _show_workfiles(): def get_context_label(): return "{0}, {1}".format( - get_current_asset_name(), + get_current_folder_path(), get_current_task_name() ) @@ -432,7 +432,7 @@ def containerise(node, ("name", name), ("namespace", namespace), ("loader", str(loader)), - ("representation", context["representation"]["_id"]), + ("representation", context["representation"]["id"]), ], **data or dict() diff --git a/client/ayon_core/hosts/nuke/api/plugin.py b/client/ayon_core/hosts/nuke/api/plugin.py index b36dfc56e6..7f016d9c66 100644 --- a/client/ayon_core/hosts/nuke/api/plugin.py +++ b/client/ayon_core/hosts/nuke/api/plugin.py @@ -95,7 +95,7 @@ class NukeCreator(NewCreator): any node having instance data knob. Arguments: - product_name (str): Subset name + product_name (str): Product name """ for node in nuke.allNodes(recurseGroups=True): diff --git a/client/ayon_core/hosts/nuke/api/workfile_template_builder.py b/client/ayon_core/hosts/nuke/api/workfile_template_builder.py index 218ba97dd5..495edd9e5f 100644 --- a/client/ayon_core/hosts/nuke/api/workfile_template_builder.py +++ b/client/ayon_core/hosts/nuke/api/workfile_template_builder.py @@ -167,7 +167,7 @@ class NukePlaceholderLoadPlugin(NukePlaceholderPlugin, PlaceholderLoadMixin): placeholder.data["nodes_init"] = nuke.allNodes() def _before_repre_load(self, placeholder, representation): - placeholder.data["last_repre_id"] = str(representation["_id"]) + placeholder.data["last_repre_id"] = representation["id"] def collect_placeholders(self): output = [] diff --git a/client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py b/client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py index f113bec887..8fb8abfbbf 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/nuke/plugins/create/convert_legacy.py @@ -1,5 +1,5 @@ from ayon_core.pipeline import AYON_INSTANCE_ID, AVALON_INSTANCE_ID -from ayon_core.pipeline.create.creator_plugins import SubsetConvertorPlugin +from ayon_core.pipeline.create.creator_plugins import ProductConvertorPlugin from ayon_core.hosts.nuke.api.lib import ( INSTANCE_DATA_KNOB, get_node_data, @@ -11,7 +11,7 @@ from ayon_core.hosts.nuke.api.plugin import convert_to_valid_instaces import nuke -class LegacyConverted(SubsetConvertorPlugin): +class LegacyConverted(ProductConvertorPlugin): identifier = "legacy.converter" def find_instances(self): diff --git a/client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py b/client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py index 0a0467787a..b9d83a2b48 100644 --- a/client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py +++ b/client/ayon_core/hosts/nuke/plugins/create/workfile_creator.py @@ -1,5 +1,6 @@ +import ayon_api + import ayon_core.hosts.nuke.api as api -from ayon_core.client import get_asset_by_name from ayon_core.pipeline import ( AutoCreator, CreatedInstance, @@ -27,27 +28,32 @@ class WorkfileCreator(AutoCreator): ) project_name = self.create_context.get_current_project_name() - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) instance_data.update({ - "asset": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant }) instance_data.update(self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, instance_data diff --git a/client/ayon_core/hosts/nuke/plugins/load/actions.py b/client/ayon_core/hosts/nuke/plugins/load/actions.py index de51321924..707a8a3c4d 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/actions.py +++ b/client/ayon_core/hosts/nuke/plugins/load/actions.py @@ -4,6 +4,7 @@ from ayon_core.lib import Logger from ayon_core.pipeline import load +from ayon_core.hosts.nuke.api import lib log = Logger.get_logger(__name__) @@ -25,14 +26,11 @@ class SetFrameRangeLoader(load.LoaderPlugin): color = "white" def load(self, context, name, namespace, data): + version_entity = context["version"] + version_attributes = version_entity["attrib"] - from ayon_core.hosts.nuke.api import lib - - version = context['version'] - version_data = version.get("data", {}) - - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + start = version_attributes.get("frameStart") + end = version_attributes.get("frameEnd") log.info("start: {}, end: {}".format(start, end)) if start is None or end is None: @@ -59,14 +57,9 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): color = "white" def load(self, context, name, namespace, data): - - from ayon_core.hosts.nuke.api import lib - - version = context['version'] - version_data = version.get("data", {}) - - start = version_data.get("frameStart", None) - end = version_data.get("frameEnd", None) + version_attributes = context["version"]["attrib"] + start = version_attributes.get("frameStart") + end = version_attributes.get("frameEnd") if start is None or end is None: print("Skipping setting frame range because start or " @@ -74,7 +67,7 @@ class SetFrameRangeWithHandlesLoader(load.LoaderPlugin): return # Include handles - start -= version_data.get("handleStart", 0) - end += version_data.get("handleEnd", 0) + start -= version_attributes.get("handleStart", 0) + end += version_attributes.get("handleEnd", 0) lib.update_frame_range(start, end) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py b/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py index 642e20c979..51fe7dac34 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_backdrop.py @@ -1,13 +1,9 @@ import nuke import nukescripts +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api.lib import ( @@ -43,7 +39,7 @@ class LoadBackdropNodes(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): namespace name data (dict): compulsory attribute > not used Returns: @@ -51,24 +47,23 @@ class LoadBackdropNodes(load.LoaderPlugin): """ # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - namespace = namespace or context['asset']['name'] - colorspace = version_data.get("colorspace", None) + namespace = namespace or context["folder"]["name"] + version_entity = context["version"] + + version_attributes = version_entity["attrib"] + colorspace = version_attributes.get("colorSpace") + object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["source", "author", "fps"] - data_imprint = { - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add attributes from the version to imprint to metadata knob + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -190,31 +185,29 @@ class LoadBackdropNodes(load.LoaderPlugin): # get main variables # Get version from io project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] # get corresponding node GN = container["node"] - file = get_representation_path(repre_doc).replace("\\", "/") + file = get_representation_path(repre_entity).replace("\\", "/") - name = container['name'] - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - namespace = container['namespace'] - colorspace = version_data.get("colorspace", None) + name = container["name"] + namespace = container["namespace"] object_name = "{}_{}".format(name, namespace) - add_keys = ["source", "author", "fps"] + version_attributes = version_entity["attrib"] + colorspace = version_attributes.get("colorSpace") data_imprint = { - "representation": str(repre_doc["_id"]), - "version": vname, + "representation": repre_entity["id"], + "version": version_entity["version"], "colorspaceInput": colorspace, } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes[k] # adding nodes to node graph # just in case we are in group lets jump out of it @@ -234,18 +227,20 @@ class LoadBackdropNodes(load.LoaderPlugin): GN["name"].setValue(object_name) # get all versions in list - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = self.node_color else: color_value = "0xd88467ff" GN["tile_color"].setValue(int(color_value, 16)) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["version"]) + ) return update_container(GN, data_imprint) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py b/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py index e3511a4e8b..bb7c8ea7c8 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_camera_abc.py @@ -1,12 +1,8 @@ import nuke +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api import ( @@ -35,27 +31,25 @@ class AlembicCameraLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - fps = version_data.get("fps") or nuke.root()["fps"].getValue() - namespace = namespace or context['asset']['name'] + version_entity = context["version"] + + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + fps = version_attributes.get("fps") or nuke.root()["fps"].getValue() + + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["source", "author", "fps"] - + # add additional metadata from the version to imprint to metadata knob data_imprint = { "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], } - - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -82,7 +76,9 @@ class AlembicCameraLoader(load.LoaderPlugin): camera_node.setXYpos(xpos, ypos) # color node by correct color by actual version - self.node_version_color(version, camera_node) + self.node_version_color( + context["project"]["name"], version_entity, camera_node + ) return containerise( node=camera_node, @@ -109,32 +105,29 @@ class AlembicCameraLoader(load.LoaderPlugin): None """ # Get version from io - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] # get main variables - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - fps = version_data.get("fps") or nuke.root()["fps"].getValue() + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + fps = version_attributes.get("fps") or nuke.root()["fps"].getValue() # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["source", "author", "fps"] - data_imprint = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": first, "frameEnd": last, - "version": vname + "version": version_entity["version"] } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add attributes from the version to imprint to metadata knob + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes[k] # getting file path - file = get_representation_path(repre_doc).replace("\\", "/") + file = get_representation_path(repre_entity).replace("\\", "/") with maintained_selection(): camera_node = container["node"] @@ -169,23 +162,26 @@ class AlembicCameraLoader(load.LoaderPlugin): d.setInput(index, camera_node) # color node by correct color by actual version - self.node_version_color(version_doc, camera_node) + self.node_version_color( + context["project"]["name"], version_entity, camera_node + ) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["version"]) + ) return update_container(camera_node, data_imprint) - def node_version_color(self, version_doc, node): + def node_version_color(self, project_name, version_entity, node): """ Coloring a node by correct color by actual version """ # get all versions in list - project_name = get_current_project_name() - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = self.node_color else: color_value = "0xd88467ff" diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py index 98c3303eb8..e972b99b85 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_clip.py @@ -1,14 +1,11 @@ +from copy import deepcopy + import nuke import qargparse -from pprint import pformat -from copy import deepcopy +import ayon_api + from ayon_core.lib import Logger -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( - get_current_project_name, get_representation_path, ) from ayon_core.pipeline.colorspace import ( @@ -88,22 +85,26 @@ class LoadClip(plugin.NukeLoader): return cls.representations_include or cls.representations def load(self, context, name, namespace, options): - """Load asset via database - """ - representation = context["representation"] + """Load asset via database.""" + project_name = context["project"]["name"] + repre_entity = context["representation"] + version_entity = context["version"] + version_attributes = version_entity["attrib"] + version_data = version_entity["data"] + # reset container id so it is always unique for each instance self.reset_container_id() - is_sequence = len(representation["files"]) > 1 + is_sequence = len(repre_entity["files"]) > 1 if is_sequence: - context["representation"] = \ - self._representation_with_hash_in_frame( - representation + context["representation"] = ( + self._representation_with_hash_in_frame(repre_entity) ) filepath = self.filepath_from_context(context) filepath = filepath.replace("\\", "/") + self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = options.get( "start_at_workfile", self.options_defaults["start_at_workfile"]) @@ -111,20 +112,16 @@ class LoadClip(plugin.NukeLoader): add_retime = options.get( "add_retime", self.options_defaults["add_retime"]) - version = context['version'] - version_data = version.get("data", {}) - repre_id = representation["_id"] + repre_id = repre_entity["id"] - self.log.debug("_ version_data: {}\n".format( - pformat(version_data))) self.log.debug( "Representation id `{}` ".format(repre_id)) - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) + self.handle_start = version_attributes.get("handleStart", 0) + self.handle_end = version_attributes.get("handleEnd", 0) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") first -= self.handle_start last += self.handle_end @@ -133,16 +130,16 @@ class LoadClip(plugin.NukeLoader): first = 1 last = first + duration - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["folder"]["name"] if not filepath: self.log.warning( "Representation id `{}` is failing to load".format(repre_id)) return - read_name = self._get_node_name(representation) + read_name = self._get_node_name(context) # Create the Loader with the filename path set read_node = nuke.createNode( @@ -151,45 +148,52 @@ class LoadClip(plugin.NukeLoader): inpanel=False ) + # get colorspace + colorspace = ( + repre_entity["data"].get("colorspace") + or version_attributes.get("colorSpace") + ) + # to avoid multiple undo steps for rest of process # we will switch off undo-ing with viewer_update_and_undo_stop(): read_node["file"].setValue(filepath) self.set_colorspace_to_node( - read_node, filepath, version, representation) + read_node, + filepath, + project_name, + version_entity, + repre_entity + ) self._set_range_to_node(read_node, first, last, start_at_workfile) - # add additional metadata from the version to imprint Avalon knob - add_keys = ["frameStart", "frameEnd", - "source", "colorspace", "author", "fps", "version", - "handleStart", "handleEnd"] + version_name = version_entity["version"] + if version_name < 0: + version_name = "hero" - data_imprint = {} - for key in add_keys: - if key == 'version': - version_doc = context["version"] - if version_doc["type"] == "hero_version": - version = "hero" - else: - version = version_doc.get("name") + data_imprint = { + "version": version_name, + "db_colorspace": colorspace + } - if version: - data_imprint[key] = version + # add attributes from the version to imprint metadata knob + for key in [ + "frameStart", + "frameEnd", + "source", + "author", + "fps", + "handleStart", + "handleEnd", + ]: + value = version_attributes.get(key, str(None)) + if isinstance(value, str): + value = value.replace("\\", "/") + data_imprint[key] = value - elif key == 'colorspace': - colorspace = representation["data"].get(key) - colorspace = colorspace or version_data.get(key) - data_imprint["db_colorspace"] = colorspace - else: - value_ = context["version"]['data'].get( - key, str(None)) - if isinstance(value_, (str)): - value_ = value_.replace("\\", "/") - data_imprint[key] = value_ - - if add_retime and version_data.get("retime", None): + if add_retime and version_data.get("retime"): data_imprint["addRetime"] = True read_node["tile_color"].setValue(int("0x4ecd25ff", 16)) @@ -202,7 +206,7 @@ class LoadClip(plugin.NukeLoader): loader=self.__class__.__name__, data=data_imprint) - if add_retime and version_data.get("retime", None): + if add_retime and version_data.get("retime"): self._make_retimes(read_node, version_data) self.set_as_member(read_node) @@ -212,17 +216,18 @@ class LoadClip(plugin.NukeLoader): def switch(self, container, context): self.update(container, context) - def _representation_with_hash_in_frame(self, representation): + def _representation_with_hash_in_frame(self, repre_entity): """Convert frame key value to padded hash Args: - representation (dict): representation data + repre_entity (dict): Representation entity. Returns: dict: altered representation data + """ - representation = deepcopy(representation) - context = representation["context"] + new_repre_entity = deepcopy(repre_entity) + context = new_repre_entity["context"] # Get the frame from the context and hash it frame = context["frame"] @@ -230,7 +235,7 @@ class LoadClip(plugin.NukeLoader): # Replace the frame with the hash in the originalBasename if ( - "{originalBasename}" in representation["data"]["template"] + "{originalBasename}" in new_repre_entity["attrib"]["template"] ): origin_basename = context["originalBasename"] context["originalBasename"] = origin_basename.replace( @@ -238,8 +243,8 @@ class LoadClip(plugin.NukeLoader): ) # Replace the frame with the hash in the frame - representation["context"]["frame"] = hashed_frame - return representation + new_repre_entity["context"]["frame"] = hashed_frame + return new_repre_entity def update(self, container, context): """Update the Loader's path @@ -250,18 +255,25 @@ class LoadClip(plugin.NukeLoader): """ - repre_doc = context["representation"] + project_name = context["project"]["name"] + version_entity = context["version"] + repre_entity = context["representation"] - is_sequence = len(repre_doc["files"]) > 1 + version_attributes = version_entity["attrib"] + version_data = version_entity["data"] + + is_sequence = len(repre_entity["files"]) > 1 read_node = container["node"] if is_sequence: - repre_doc = self._representation_with_hash_in_frame( - repre_doc + repre_entity = self._representation_with_hash_in_frame( + repre_entity ) - filepath = get_representation_path(repre_doc).replace("\\", "/") + filepath = ( + get_representation_path(repre_entity) + ).replace("\\", "/") self.log.debug("_ filepath: {}".format(filepath)) start_at_workfile = "start at" in read_node['frame_mode'].value() @@ -271,21 +283,19 @@ class LoadClip(plugin.NukeLoader): if "addRetime" in key ] - project_name = get_current_project_name() - version_doc = get_version_by_id(project_name, repre_doc["parent"]) - - version_data = version_doc.get("data", {}) - repre_id = repre_doc["_id"] + repre_id = repre_entity["id"] # colorspace profile - colorspace = repre_doc["data"].get("colorspace") - colorspace = colorspace or version_data.get("colorspace") + colorspace = ( + repre_entity["data"].get("colorspace") + or version_attributes.get("colorSpace") + ) - self.handle_start = version_data.get("handleStart", 0) - self.handle_end = version_data.get("handleEnd", 0) + self.handle_start = version_attributes.get("handleStart", 0) + self.handle_end = version_attributes.get("handleEnd", 0) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") first -= self.handle_start last += self.handle_end @@ -305,44 +315,45 @@ class LoadClip(plugin.NukeLoader): # we will switch off undo-ing with viewer_update_and_undo_stop(): self.set_colorspace_to_node( - read_node, filepath, version_doc, repre_doc + read_node, + filepath, + project_name, + version_entity, + repre_entity ) self._set_range_to_node(read_node, first, last, start_at_workfile) updated_dict = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": str(first), "frameEnd": str(last), - "version": str(version_doc.get("name")), + "version": str(version_entity["version"]), "db_colorspace": colorspace, - "source": version_data.get("source"), + "source": version_attributes.get("source"), "handleStart": str(self.handle_start), "handleEnd": str(self.handle_end), - "fps": str(version_data.get("fps")), - "author": version_data.get("author") + "fps": str(version_attributes.get("fps")), + "author": version_attributes.get("author") } - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of read_node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = "0x4ecd25ff" else: color_value = "0xd84f20ff" read_node["tile_color"].setValue(int(color_value, 16)) # Update the imprinted representation - update_container( - read_node, - updated_dict - ) + update_container(read_node, updated_dict) self.log.info( - "updated to version: {}".format(version_doc.get("name")) + "updated to version: {}".format(version_entity["version"]) ) - if add_retime and version_data.get("retime", None): + if add_retime and version_data.get("retime"): self._make_retimes(read_node, version_data) else: self.clear_members(read_node) @@ -350,11 +361,12 @@ class LoadClip(plugin.NukeLoader): self.set_as_member(read_node) def set_colorspace_to_node( - self, - read_node, - filepath, - version_doc, - representation_doc, + self, + read_node, + filepath, + project_name, + version_entity, + repre_entity, ): """Set colorspace to read node. @@ -362,14 +374,15 @@ class LoadClip(plugin.NukeLoader): Args: read_node (nuke.Node): The nuke's read node - filepath (str): file path - version_doc (dict): version document - representation_doc (dict): representation document + filepath (str): File path. + project_name (str): Project name. + version_entity (dict): Version entity. + repre_entity (dict): Representation entity. """ used_colorspace = self._get_colorspace_data( - version_doc, representation_doc, filepath) - + project_name, version_entity, repre_entity, filepath + ) if ( used_colorspace and colorspace_exists_on_node(read_node, used_colorspace) @@ -465,52 +478,69 @@ class LoadClip(plugin.NukeLoader): read_node['frame_mode'].setValue("start at") read_node['frame'].setValue(str(self.script_start)) - def _get_node_name(self, representation): + def _get_node_name(self, context): + folder_entity = context["folder"] + product_name = context["product"]["name"] + repre_entity = context["representation"] - repre_cont = representation["context"] + folder_name = folder_entity["name"] + repre_cont = repre_entity["context"] name_data = { - "asset": repre_cont["asset"], - "subset": repre_cont["subset"], - "representation": representation["name"], + "folder": { + "name": folder_name, + }, + "product": { + "name": product_name, + }, + "asset": folder_name, + "subset": product_name, + "representation": repre_entity["name"], "ext": repre_cont["representation"], - "id": representation["_id"], + "id": repre_entity["id"], "class_name": self.__class__.__name__ } return self.node_name_template.format(**name_data) - def _get_colorspace_data(self, version_doc, representation_doc, filepath): + def _get_colorspace_data( + self, project_name, version_entity, repre_entity, filepath + ): """Get colorspace data from version and representation documents Args: - version_doc (dict): version document - representation_doc (dict): representation document - filepath (str): file path + project_name (str): Project name. + version_entity (dict): Version entity. + repre_entity (dict): Representation entity. + filepath (str): File path. Returns: Any[str,None]: colorspace name or None """ # Get backward compatible colorspace key. - colorspace = representation_doc["data"].get("colorspace") + colorspace = repre_entity["data"].get("colorspace") self.log.debug( f"Colorspace from representation colorspace: {colorspace}" ) # Get backward compatible version data key if colorspace is not found. - colorspace = colorspace or version_doc["data"].get("colorspace") - self.log.debug(f"Colorspace from version colorspace: {colorspace}") + if not colorspace: + colorspace = version_entity["attrib"].get("colorSpace") + self.log.debug( + f"Colorspace from version colorspace: {colorspace}" + ) # Get colorspace from representation colorspaceData if colorspace is # not found. - colorspace_data = representation_doc["data"].get("colorspaceData", {}) - colorspace = colorspace or colorspace_data.get("colorspace") - self.log.debug( - f"Colorspace from representation colorspaceData: {colorspace}" - ) + if not colorspace: + colorspace_data = repre_entity["data"].get("colorspaceData", {}) + colorspace = colorspace_data.get("colorspace") + self.log.debug( + f"Colorspace from representation colorspaceData: {colorspace}" + ) # check if any filerules are not applicable new_parsed_colorspace = get_imageio_file_rules_colorspace_from_filepath( # noqa - filepath, "nuke", get_current_project_name() + filepath, "nuke", project_name ) self.log.debug(f"Colorspace new filerules: {new_parsed_colorspace}") diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_effects.py b/client/ayon_core/hosts/nuke/plugins/load/load_effects.py index 3e87c9cf60..50ce0d1ad7 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_effects.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_effects.py @@ -2,14 +2,10 @@ import json from collections import OrderedDict import nuke import six +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api import ( @@ -40,37 +36,43 @@ class LoadEffects(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): namespace name data (dict): compulsory attribute > not used Returns: nuke node: containerised nuke node object """ # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) + version_entity = context["version"] + + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - namespace = namespace or context['asset']['name'] - colorspace = version_data.get("colorspace", None) + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] - data_imprint = { "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace, } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add additional metadata from the version to imprint to Avalon knob + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -157,36 +159,40 @@ class LoadEffects(load.LoaderPlugin): # get main variables # Get version from io project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] # get corresponding node GN = container["node"] - file = get_representation_path(repre_doc).replace("\\", "/") - name = container['name'] - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - namespace = container['namespace'] - colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) + file = get_representation_path(repre_entity).replace("\\", "/") - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") + + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) + namespace = container["namespace"] data_imprint = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps", + ]: + data_imprint[k] = version_attributes[k] # Update the imprinted representation update_container( @@ -252,32 +258,34 @@ class LoadEffects(load.LoaderPlugin): # try to find parent read node self.connect_read_node(GN, namespace, json_f["assignTo"]) - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = "0x3469ffff" else: color_value = "0xd84f20ff" GN["tile_color"].setValue(int(color_value, 16)) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["version"]) + ) - def connect_read_node(self, group_node, asset, subset): + def connect_read_node(self, group_node, namespace, product_name): """ Finds read node and selects it Arguments: - asset (str): asset name + namespace (str): namespace name Returns: nuke node: node is selected None: if nothing found """ - search_name = "{0}_{1}".format(asset, subset) + search_name = "{0}_{1}".format(namespace, product_name) node = [ n for n in nuke.allNodes(filter="Read") diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py b/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py index 5c363cddc4..8c58241316 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_effects_ip.py @@ -2,14 +2,10 @@ import json from collections import OrderedDict import six import nuke +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api import lib @@ -40,7 +36,7 @@ class LoadEffectsInputProcess(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): namespace name data (dict): compulsory attribute > not used Returns: @@ -48,30 +44,35 @@ class LoadEffectsInputProcess(load.LoaderPlugin): """ # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) + version_entity = context["version"] + + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - namespace = namespace or context['asset']['name'] - colorspace = version_data.get("colorspace", None) + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] - data_imprint = { "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace, } - - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add additional metadata from the version to imprint to Avalon knob + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -162,33 +163,39 @@ class LoadEffectsInputProcess(load.LoaderPlugin): # get main variables # Get version from io project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] # get corresponding node GN = container["node"] - file = get_representation_path(repre_doc).replace("\\", "/") - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - workfile_first_frame = int(nuke.root()["first_frame"].getValue()) - colorspace = version_data.get("colorspace", None) + file = get_representation_path(repre_entity).replace("\\", "/") - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") + + workfile_first_frame = int(nuke.root()["first_frame"].getValue()) data_imprint = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace, } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # Update the imprinted representation update_container( @@ -252,18 +259,18 @@ class LoadEffectsInputProcess(load.LoaderPlugin): output.setInput(0, pre_node) # get all versions in list - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = "0x3469ffff" else: color_value = "0xd84f20ff" GN["tile_color"].setValue(int(color_value, 16)) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info("updated to version: {}".format(version_entity["name"])) def connect_active_viewer(self, group_node): """ diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py b/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py index 058228a145..5130b825c4 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_gizmo.py @@ -1,12 +1,8 @@ import nuke +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api.lib import ( @@ -42,7 +38,7 @@ class LoadGizmo(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): namespace name data (dict): compulsory attribute > not used Returns: @@ -50,29 +46,35 @@ class LoadGizmo(load.LoaderPlugin): """ # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - namespace = namespace or context['asset']['name'] - colorspace = version_data.get("colorspace", None) + version_entity = context["version"] + version_attributes = version_entity["attrib"] + + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") + + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] - data_imprint = { "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add attributes from the version to imprint to metadata knob + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -109,35 +111,38 @@ class LoadGizmo(load.LoaderPlugin): # get main variables # Get version from io project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] + + version_attributes = version_entity["attrib"] # get corresponding node group_node = container["node"] - file = get_representation_path(repre_doc).replace("\\", "/") - name = container['name'] - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - namespace = container['namespace'] - colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) + file = get_representation_path(repre_entity).replace("\\", "/") - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") data_imprint = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # capture pipeline metadata avalon_data = get_avalon_knob_data(group_node) @@ -158,19 +163,21 @@ class LoadGizmo(load.LoaderPlugin): # set updated pipeline metadata set_avalon_knob_data(new_group_node, avalon_data) - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = self.node_color else: color_value = "0xd88467ff" new_group_node["tile_color"].setValue(int(color_value, 16)) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["name"]) + ) return update_container(new_group_node, data_imprint) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py b/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py index 61e1c34028..8aa73ca0e4 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_gizmo_ip.py @@ -1,13 +1,9 @@ import nuke import six +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api.lib import ( @@ -44,7 +40,7 @@ class LoadGizmoInputProcess(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): namespace name data (dict): compulsory attribute > not used Returns: @@ -52,29 +48,35 @@ class LoadGizmoInputProcess(load.LoaderPlugin): """ # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - namespace = namespace or context['asset']['name'] - colorspace = version_data.get("colorspace", None) + version_entity = context["version"] + + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") + + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] - + # add additional metadata from the version to imprint to metadata knob data_imprint = { "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -116,35 +118,37 @@ class LoadGizmoInputProcess(load.LoaderPlugin): # get main variables # Get version from io project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] # get corresponding node group_node = container["node"] - file = get_representation_path(repre_doc).replace("\\", "/") - name = container['name'] - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - namespace = container['namespace'] - colorspace = version_data.get("colorspace", None) - object_name = "{}_{}".format(name, namespace) + file = get_representation_path(repre_entity).replace("\\", "/") - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") data_imprint = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": first, "frameEnd": last, - "version": vname, + "version": version_entity["version"], "colorspaceInput": colorspace } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # capture pipeline metadata avalon_data = get_avalon_knob_data(group_node) @@ -165,18 +169,20 @@ class LoadGizmoInputProcess(load.LoaderPlugin): # set updated pipeline metadata set_avalon_knob_data(new_group_node, avalon_data) - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = self.node_color else: color_value = "0xd88467ff" new_group_node["tile_color"].setValue(int(color_value, 16)) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["version"]) + ) return update_container(new_group_node, data_imprint) diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_image.py b/client/ayon_core/hosts/nuke/plugins/load/load_image.py index 4f7a5ccc27..002592758f 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_image.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_image.py @@ -1,14 +1,10 @@ import nuke import qargparse +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api.lib import ( @@ -72,40 +68,37 @@ class LoadImage(load.LoaderPlugin): "frame_number", int(nuke.root()["first_frame"].getValue()) ) - version = context['version'] - version_data = version.get("data", {}) - repr_id = context["representation"]["_id"] + version_entity = context["version"] + version_attributes = version_entity["attrib"] + repre_entity = context["representation"] + repre_id = repre_entity["id"] - self.log.info("version_data: {}\n".format(version_data)) self.log.debug( - "Representation id `{}` ".format(repr_id)) + "Representation id `{}` ".format(repre_id)) last = first = int(frame_number) - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["folder"]["name"] file = self.filepath_from_context(context) if not file: - repr_id = context["representation"]["_id"] self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) + "Representation id `{}` is failing to load".format(repre_id)) return file = file.replace("\\", "/") - representation = context["representation"] - repr_cont = representation["context"] - frame = repr_cont.get("frame") + frame = repre_entity["context"].get("frame") if frame: padding = len(frame) file = file.replace( frame, format(frame_number, "0{}".format(padding))) - read_name = self._get_node_name(representation) + read_name = self._get_node_name(context) # Create the Loader with the filename path set with viewer_update_and_undo_stop(): @@ -118,7 +111,7 @@ class LoadImage(load.LoaderPlugin): r["file"].setValue(file) # Set colorspace defined in version data - colorspace = context["version"]["data"].get("colorspace") + colorspace = version_entity["attrib"].get("colorSpace") if colorspace: r["colorspace"].setValue(str(colorspace)) @@ -132,19 +125,16 @@ class LoadImage(load.LoaderPlugin): r["origlast"].setValue(last) r["last"].setValue(last) - # add additional metadata from the version to imprint Avalon knob - add_keys = ["source", "colorspace", "author", "fps", "version"] - + # add attributes from the version to imprint metadata knob + colorspace = version_attributes["colorSpace"] data_imprint = { "frameStart": first, - "frameEnd": last + "frameEnd": last, + "version": version_entity["version"], + "colorspace": colorspace, } - for k in add_keys: - if k == 'version': - data_imprint.update({k: context["version"]['name']}) - else: - data_imprint.update( - {k: context["version"]['data'].get(k, str(None))}) + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes.get(k, str(None)) r["tile_color"].setValue(int("0x4ecd25ff", 16)) @@ -172,17 +162,17 @@ class LoadImage(load.LoaderPlugin): assert node.Class() == "Read", "Must be Read" project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] - repr_cont = repre_doc["context"] + repr_cont = repre_entity["context"] - file = get_representation_path(repre_doc) + file = get_representation_path(repre_entity) if not file: - repr_id = repre_doc["_id"] + repre_id = repre_entity["id"] self.log.warning( - "Representation id `{}` is failing to load".format(repr_id)) + "Representation id `{}` is failing to load".format(repre_id)) return file = file.replace("\\", "/") @@ -195,12 +185,10 @@ class LoadImage(load.LoaderPlugin): format(frame_number, "0{}".format(padding))) # Get start frame from version data - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) - version_data = version_doc.get("data", {}) - last = first = int(frame_number) # Set the global in to the start frame of the sequence @@ -210,31 +198,30 @@ class LoadImage(load.LoaderPlugin): node["origlast"].setValue(last) node["last"].setValue(last) - updated_dict = {} - updated_dict.update({ - "representation": str(repre_doc["_id"]), + version_attributes = version_entity["attrib"] + updated_dict = { + "representation": repre_entity["id"], "frameStart": str(first), "frameEnd": str(last), - "version": str(version_doc.get("name")), - "colorspace": version_data.get("colorspace"), - "source": version_data.get("source"), - "fps": str(version_data.get("fps")), - "author": version_data.get("author") - }) + "version": str(version_entity["version"]), + "colorspace": version_attributes.get("colorSpace"), + "source": version_attributes.get("source"), + "fps": str(version_attributes.get("fps")), + "author": version_attributes.get("author") + } # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = "0x4ecd25ff" else: color_value = "0xd84f20ff" node["tile_color"].setValue(int(color_value, 16)) # Update the imprinted representation - update_container( - node, - updated_dict - ) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + update_container(node, updated_dict) + self.log.info("updated to version: {}".format( + version_entity["version"] + )) def remove(self, container): node = container["node"] @@ -243,15 +230,25 @@ class LoadImage(load.LoaderPlugin): with viewer_update_and_undo_stop(): nuke.delete(node) - def _get_node_name(self, representation): + def _get_node_name(self, context): + folder_entity = context["folder"] + product_name = context["product"]["name"] + repre_entity = context["representation"] - repre_cont = representation["context"] + folder_name = folder_entity["name"] + repre_cont = repre_entity["context"] name_data = { - "asset": repre_cont["asset"], - "subset": repre_cont["subset"], - "representation": representation["name"], + "folder": { + "name": folder_name, + }, + "product": { + "name": product_name, + }, + "asset": folder_name, + "subset": product_name, + "representation": repre_entity["name"], "ext": repre_cont["representation"], - "id": representation["_id"], + "id": repre_entity["id"], "class_name": self.__class__.__name__ } diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_model.py b/client/ayon_core/hosts/nuke/plugins/load/load_model.py index cd4b72df91..fcf1c2eda9 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_model.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_model.py @@ -1,12 +1,8 @@ import nuke +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api.lib import maintained_selection @@ -33,27 +29,26 @@ class AlembicModelLoader(load.LoaderPlugin): def load(self, context, name, namespace, data): # get main variables - version = context['version'] - version_data = version.get("data", {}) - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - fps = version_data.get("fps") or nuke.root()["fps"].getValue() - namespace = namespace or context['asset']['name'] + project_name = context["project"]["name"] + version_entity = context["version"] + + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + fps = version_attributes.get("fps") or nuke.root()["fps"].getValue() + + namespace = namespace or context["folder"]["name"] object_name = "{}_{}".format(name, namespace) # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["source", "author", "fps"] - data_imprint = { "frameStart": first, "frameEnd": last, - "version": vname + "version": version_entity["version"] } - - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add attributes from the version to imprint to metadata knob + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes[k] # getting file path file = self.filepath_from_context(context).replace("\\", "/") @@ -86,7 +81,7 @@ class AlembicModelLoader(load.LoaderPlugin): model_node.setXYpos(xpos, ypos) # color node by correct color by actual version - self.node_version_color(version, model_node) + self.node_version_color(project_name, version_entity, model_node) return containerise( node=model_node, @@ -113,35 +108,33 @@ class AlembicModelLoader(load.LoaderPlugin): None """ # Get version from io - version_doc = context["version"] - repre_doc = context["representation"] + project_name = context["project"]["name"] + version_entity = context["version"] + repre_entity = context["representation"] # get corresponding node model_node = container["node"] # get main variables - version_data = version_doc.get("data", {}) - vname = version_doc.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) - fps = version_data.get("fps") or nuke.root()["fps"].getValue() + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + fps = version_attributes.get("fps") or nuke.root()["fps"].getValue() # prepare data for imprinting - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["source", "author", "fps"] - data_imprint = { - "representation": str(repre_doc["_id"]), + "representation": repre_entity["id"], "frameStart": first, "frameEnd": last, - "version": vname + "version": version_entity["version"] } - for k in add_keys: - data_imprint.update({k: version_data[k]}) + # add additional metadata from the version to imprint to Avalon knob + for k in ["source", "author", "fps"]: + data_imprint[k] = version_attributes[k] # getting file path - file = get_representation_path(repre_doc).replace("\\", "/") + file = get_representation_path(repre_entity).replace("\\", "/") with maintained_selection(): model_node['selected'].setValue(True) @@ -181,22 +174,23 @@ class AlembicModelLoader(load.LoaderPlugin): d.setInput(index, model_node) # color node by correct color by actual version - self.node_version_color(version_doc, model_node) + self.node_version_color(project_name, version_entity, model_node) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["version"]) + ) return update_container(model_node, data_imprint) - def node_version_color(self, version, node): + def node_version_color(self, project_name, version_entity, node): """ Coloring a node by correct color by actual version""" - project_name = get_current_project_name() - last_version_doc = get_last_version_by_subset_id( - project_name, version["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = self.node_color else: color_value = "0xd88467ff" diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py b/client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py index e2e7cd3262..94af7c40e2 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_ociolook.py @@ -1,16 +1,13 @@ import os import json import secrets + import nuke import six +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id -) from ayon_core.pipeline import ( load, - get_current_project_name, get_representation_path, ) from ayon_core.hosts.nuke.api import ( @@ -47,13 +44,13 @@ class LoadOcioLookNodes(load.LoaderPlugin): Arguments: context (dict): context of version name (str): name of the version - namespace (str): asset name + namespace (str): namespace name data (dict): compulsory attribute > not used Returns: nuke.Node: containerized nuke.Node object """ - namespace = namespace or context['asset']['name'] + namespace = namespace or context["folder"]["name"] suffix = secrets.token_hex(nbytes=4) node_name = "{}_{}_{}".format( name, namespace, suffix) @@ -68,7 +65,11 @@ class LoadOcioLookNodes(load.LoaderPlugin): # renaming group node group_node["name"].setValue(node_name) - self._node_version_color(context["version"], group_node) + self._node_version_color( + context["project"]["name"], + context["version"], + group_node + ) self.log.info( "Loaded lut setup: `{}`".format(group_node["name"].value())) @@ -220,12 +221,11 @@ class LoadOcioLookNodes(load.LoaderPlugin): return group_node def update(self, container, context): - version_doc = context["version"] - repre_doc = context["representation"] + repre_entity = context["representation"] group_node = container["node"] - filepath = get_representation_path(repre_doc) + filepath = get_representation_path(repre_entity) json_f = self._load_json_data(filepath) @@ -235,13 +235,15 @@ class LoadOcioLookNodes(load.LoaderPlugin): group_node ) - self._node_version_color(version_doc, group_node) + self._node_version_color( + context["project"]["name"], context["version"], group_node + ) self.log.info("Updated lut setup: `{}`".format( group_node["name"].value())) return update_container( - group_node, {"representation": str(repre_doc["_id"])}) + group_node, {"representation": repre_entity["id"]}) def _load_json_data(self, filepath): # getting data from json file with unicode conversion @@ -287,16 +289,15 @@ class LoadOcioLookNodes(load.LoaderPlugin): with viewer_update_and_undo_stop(): nuke.delete(node) - def _node_version_color(self, version, node): + def _node_version_color(self, project_name, version_entity, node): """ Coloring a node by correct color by actual version""" - project_name = get_current_project_name() - last_version_doc = get_last_version_by_subset_id( - project_name, version["parent"], fields=["_id"] + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} ) # change color of node - if version["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = self.current_node_color else: color_value = self.old_node_color diff --git a/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py b/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py index 5d62a7ca0f..9fb168f322 100644 --- a/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py +++ b/client/ayon_core/hosts/nuke/plugins/load/load_script_precomp.py @@ -1,11 +1,7 @@ import nuke +import ayon_api -from ayon_core.client import ( - get_version_by_id, - get_last_version_by_subset_id, -) from ayon_core.pipeline import ( - get_current_project_name, load, get_representation_path, ) @@ -32,34 +28,37 @@ class LinkAsGroup(load.LoaderPlugin): def load(self, context, name, namespace, data): # for k, v in context.items(): # log.info("key: `{}`, value: {}\n".format(k, v)) - version = context['version'] - version_data = version.get("data", {}) + version_entity = context["version"] - vname = version.get("name", None) - first = version_data.get("frameStart", None) - last = version_data.get("frameEnd", None) + version_attributes = version_entity["attrib"] + first = version_attributes.get("frameStart") + last = version_attributes.get("frameEnd") + colorspace = version_attributes.get("colorSpace") - # Fallback to asset name when namespace is None + # Fallback to folder name when namespace is None if namespace is None: - namespace = context['asset']['name'] + namespace = context["folder"]["name"] file = self.filepath_from_context(context).replace("\\", "/") self.log.info("file: {}\n".format(file)) - self.log.info("versionData: {}\n".format(context["version"]["data"])) - - # add additional metadata from the version to imprint to Avalon knob - add_keys = ["frameStart", "frameEnd", "handleStart", "handleEnd", - "source", "author", "fps"] - data_imprint = { - "startingFrame": first, - "frameStart": first, - "frameEnd": last, - "version": vname + "startingFrame": first, + "frameStart": first, + "frameEnd": last, + "version": version_entity["version"] } - for k in add_keys: - data_imprint.update({k: context["version"]['data'][k]}) + # add additional metadata from the version to imprint to Avalon knob + for k in [ + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "source", + "author", + "fps" + ]: + data_imprint[k] = version_attributes[k] # group context is set to precomp, so back up one level. nuke.endGroup() @@ -72,7 +71,6 @@ class LinkAsGroup(load.LoaderPlugin): ) # Set colorspace defined in version data - colorspace = context["version"]["data"].get("colorspace", None) self.log.info("colorspace: {}\n".format(colorspace)) P["name"].setValue("{}_{}".format(name, namespace)) @@ -118,27 +116,23 @@ class LinkAsGroup(load.LoaderPlugin): node = container["node"] project_name = context["project"]["name"] - version_doc = context["version"] - repre_doc = context["representation"] + version_entity = context["version"] + repre_entity = context["representation"] - root = get_representation_path(repre_doc).replace("\\", "/") + root = get_representation_path(repre_entity).replace("\\", "/") # Get start frame from version data - last_version_doc = get_last_version_by_subset_id( - project_name, version_doc["parent"], fields=["_id"] - ) - updated_dict = {} - version_data = version_doc["data"] - updated_dict.update({ - "representation": str(repre_doc["_id"]), - "frameEnd": version_data.get("frameEnd"), - "version": version_doc.get("name"), - "colorspace": version_data.get("colorspace"), - "source": version_data.get("source"), - "fps": version_data.get("fps"), - "author": version_data.get("author") - }) + version_attributes = version_entity["attrib"] + updated_dict = { + "representation": repre_entity["id"], + "frameEnd": version_attributes.get("frameEnd"), + "version": version_entity["version"], + "colorspace": version_attributes.get("colorSpace"), + "source": version_attributes.get("source"), + "fps": version_attributes.get("fps"), + "author": version_attributes.get("author") + } # Update the imprinted representation update_container( @@ -148,14 +142,19 @@ class LinkAsGroup(load.LoaderPlugin): node["file"].setValue(root) + last_version_entity = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"], fields={"id"} + ) # change color of node - if version_doc["_id"] == last_version_doc["_id"]: + if version_entity["id"] == last_version_entity["id"]: color_value = "0xff0ff0ff" else: color_value = "0xd84f20ff" node["tile_color"].setValue(int(color_value, 16)) - self.log.info("updated to version: {}".format(version_doc.get("name"))) + self.log.info( + "updated to version: {}".format(version_entity["version"]) + ) def remove(self, container): node = container["node"] diff --git a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py b/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py index 8ac07c641c..8d7a3ec311 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/extract_review_intermediates.py @@ -109,7 +109,7 @@ class ExtractReviewIntermediates(publish.Extractor): if f_task_types and task_type not in f_task_types: continue - # test subsets from filter + # test products from filter if product_names and not any( re.search(p, product_name) for p in product_names ): diff --git a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_asset_context.xml b/client/ayon_core/hosts/nuke/plugins/publish/help/validate_asset_context.xml index d9394ae510..1e7d340a13 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/help/validate_asset_context.xml +++ b/client/ayon_core/hosts/nuke/plugins/publish/help/validate_asset_context.xml @@ -1,13 +1,13 @@ - Shot/Asset name + Folder path -## Publishing to a different asset context +## Publishing to a different folder context -There are publish instances present which are publishing into a different asset than your current context. +There are publish instances present which are publishing into a different folder than your current context. -Usually this is not what you want but there can be cases where you might want to publish into another asset/shot or task. +Usually this is not what you want but there can be cases where you might want to publish into another folder/shot or task. If that's the case you can disable the validation on the instance to ignore it. diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py b/client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py index 52ef4a58d4..93a30aa438 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/validate_asset_context.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Validate if instance asset is the same as context asset.""" +"""Validate if instance folder is the same as context folder.""" from __future__ import absolute_import import pyblish.api @@ -17,10 +17,10 @@ class ValidateCorrectAssetContext( pyblish.api.InstancePlugin, OptionalPyblishPluginMixin ): - """Validator to check if instance asset context match context asset. + """Validator to check if instance folder context match context folder. When working in per-shot style you always publish data in context of - current asset (shot). This validator checks if this is so. It is optional + current folder (shot). This validator checks if this is so. It is optional so it can be disabled when needed. Checking `folderPath` and `task` keys. diff --git a/client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py b/client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py index c4974817bd..2bd2034079 100644 --- a/client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py +++ b/client/ayon_core/hosts/nuke/plugins/publish/validate_script_attributes.py @@ -29,7 +29,7 @@ class ValidateScriptAttributes( script_data = deepcopy(instance.context.data["scriptData"]) - asset = instance.data["assetEntity"] + src_folder_attributes = instance.data["folderEntity"]["attrib"] # These attributes will be checked attributes = [ @@ -42,32 +42,32 @@ class ValidateScriptAttributes( "handleEnd" ] - # get only defined attributes from asset data - asset_attributes = { - attr: asset["data"][attr] + # get only defined attributes from folder data + folder_attributes = { + attr: src_folder_attributes[attr] for attr in attributes - if attr in asset["data"] + if attr in src_folder_attributes } # fix frame values to include handles - asset_attributes["fps"] = float("{0:.4f}".format( - asset_attributes["fps"])) + folder_attributes["fps"] = float("{0:.4f}".format( + folder_attributes["fps"])) script_data["fps"] = float("{0:.4f}".format( script_data["fps"])) - # Compare asset's values Nukescript X Database + # Compare folder's values Nukescript X Database not_matching = [] for attr in attributes: self.log.debug( - "Asset vs Script attribute \"{}\": {}, {}".format( + "Folder vs Script attribute \"{}\": {}, {}".format( attr, - asset_attributes[attr], + folder_attributes[attr], script_data[attr] ) ) - if asset_attributes[attr] != script_data[attr]: + if folder_attributes[attr] != script_data[attr]: not_matching.append({ "name": attr, - "expected": asset_attributes[attr], + "expected": folder_attributes[attr], "actual": script_data[attr] }) diff --git a/client/ayon_core/hosts/nuke/startup/custom_write_node.py b/client/ayon_core/hosts/nuke/startup/custom_write_node.py index 84e99f34c4..075c8e7a17 100644 --- a/client/ayon_core/hosts/nuke/startup/custom_write_node.py +++ b/client/ayon_core/hosts/nuke/startup/custom_write_node.py @@ -145,8 +145,8 @@ class WriteNodeKnobSettingPanel(nukescripts.PythonPanel): for setting in settings: # TODO change 'subsets' to 'product_names' in settings - for subset in setting["subsets"]: - preset_name.append(subset) + for product_name in setting["subsets"]: + preset_name.append(product_name) return preset_name, knobs_nodes diff --git a/client/ayon_core/hosts/photoshop/api/README.md b/client/ayon_core/hosts/photoshop/api/README.md index 9383bdaade..c936c1ec1f 100644 --- a/client/ayon_core/hosts/photoshop/api/README.md +++ b/client/ayon_core/hosts/photoshop/api/README.md @@ -226,14 +226,14 @@ class ImageLoader(load.LoaderPlugin): def update(self, container, context): layer = container.pop("layer") - repre_doc = context["representation"] + repre_entity = context["representation"] with photoshop.maintained_selection(): stub.replace_smart_object( - layer, get_representation_path(repre_doc) + layer, get_representation_path(repre_entity) ) stub.imprint( - layer, {"representation": str(repre_doc["_id"])} + layer, {"representation": repre_entity["id"]} ) def remove(self, container): diff --git a/client/ayon_core/hosts/photoshop/api/extension/host/index.jsx b/client/ayon_core/hosts/photoshop/api/extension/host/index.jsx index e2711fb960..b697ee65ab 100644 --- a/client/ayon_core/hosts/photoshop/api/extension/host/index.jsx +++ b/client/ayon_core/hosts/photoshop/api/extension/host/index.jsx @@ -213,7 +213,7 @@ function getActiveDocumentFullName(){ function imprint(payload){ /** * Sets headline content of current document with metadata. Stores - * information about assets created through Avalon. + * information about assets created through AYON. * Content accessible in PS through File > File Info * **/ diff --git a/client/ayon_core/hosts/photoshop/api/launch_logic.py b/client/ayon_core/hosts/photoshop/api/launch_logic.py index adf90be311..17fe7d5920 100644 --- a/client/ayon_core/hosts/photoshop/api/launch_logic.py +++ b/client/ayon_core/hosts/photoshop/api/launch_logic.py @@ -8,6 +8,7 @@ from wsrpc_aiohttp import ( WebSocketAsync ) +import ayon_api from qtpy import QtCore from ayon_core.lib import Logger, StringTemplate @@ -23,7 +24,6 @@ from ayon_core.pipeline.template_data import get_template_data_with_names from ayon_core.tools.utils import host_tools from ayon_core.tools.adobe_webserver.app import WebServerTool from ayon_core.pipeline.context_tools import change_current_context -from ayon_core.client import get_asset_by_name from .ws_stub import PhotoshopServerStub @@ -318,25 +318,28 @@ class PhotoshopRoute(WebSocketRoute): # This method calls function on the client side # client functions - async def set_context(self, project, asset, task): + async def set_context(self, project, folder, task): """ - Sets 'project' and 'asset' to envs, eg. setting context. + Sets 'project' and 'folder' to envs, eg. setting context. Opens last workile from that context if exists. Args: project (str) - asset (str) + folder (str) task (str """ log.info("Setting context change") - log.info(f"project {project} asset {asset} task {task}") + log.info(f"project {project} folder {folder} task {task}") - asset_doc = get_asset_by_name(project, asset) - change_current_context(asset_doc, task) + folder_entity = ayon_api.get_folder_by_path(project, folder) + task_entity = ayon_api.get_task_by_name( + project, folder_entity["id"], task + ) + change_current_context(folder_entity, task_entity) last_workfile_path = self._get_last_workfile_path(project, - asset, + folder, task) if last_workfile_path and os.path.exists(last_workfile_path): ProcessLauncher.execute_in_main_thread( @@ -372,20 +375,20 @@ class PhotoshopRoute(WebSocketRoute): # Required return statement. return "nothing" - def _get_last_workfile_path(self, project_name, asset_name, task_name): + def _get_last_workfile_path(self, project_name, folder_path, task_name): """Returns last workfile path if exists""" host = registered_host() host_name = "photoshop" template_key = get_workfile_template_key_from_context( - asset_name, + project_name, + folder_path, task_name, host_name, - project_name=project_name ) anatomy = Anatomy(project_name) data = get_template_data_with_names( - project_name, asset_name, task_name, host_name + project_name, folder_path, task_name, host_name ) data["root"] = anatomy.roots diff --git a/client/ayon_core/hosts/photoshop/api/pipeline.py b/client/ayon_core/hosts/photoshop/api/pipeline.py index 4e9a861220..32f66cf7fb 100644 --- a/client/ayon_core/hosts/photoshop/api/pipeline.py +++ b/client/ayon_core/hosts/photoshop/api/pipeline.py @@ -260,7 +260,7 @@ def containerise( "name": name, "namespace": namespace, "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], "members": [str(layer.id)] } stub = lib.stub() diff --git a/client/ayon_core/hosts/photoshop/api/plugin.py b/client/ayon_core/hosts/photoshop/api/plugin.py index d4eb38300f..c11a206834 100644 --- a/client/ayon_core/hosts/photoshop/api/plugin.py +++ b/client/ayon_core/hosts/photoshop/api/plugin.py @@ -4,21 +4,21 @@ from ayon_core.pipeline import LoaderPlugin from .launch_logic import stub -def get_unique_layer_name(layers, asset_name, product_name): +def get_unique_layer_name(layers, container_name, product_name): """Prepare unique layer name. - Gets all layer names and if '_' is present, + Gets all layer names and if '_' is present, it adds suffix '1', or increases the suffix by 1. Args: layers (list) of dict with layers info (name, id etc.) - asset_name (str): + container_name (str): product_name (str): Returns: str: name_00X (without version) """ - name = "{}_{}".format(asset_name, product_name) + name = "{}_{}".format(container_name, product_name) names = {} for layer in layers: layer_name = re.sub(r'_\d{3}$', '', layer.name) diff --git a/client/ayon_core/hosts/photoshop/lib.py b/client/ayon_core/hosts/photoshop/lib.py index 6d5be48bc2..dd227c5d81 100644 --- a/client/ayon_core/hosts/photoshop/lib.py +++ b/client/ayon_core/hosts/photoshop/lib.py @@ -1,7 +1,8 @@ import re +import ayon_api + import ayon_core.hosts.photoshop.api as api -from ayon_core.client import get_asset_by_name from ayon_core.lib import prepare_template_data from ayon_core.pipeline import ( AutoCreator, @@ -40,33 +41,38 @@ class PSAutoCreator(AutoCreator): context = self.create_context project_name = context.get_current_project_name() - asset_name = context.get_current_asset_name() + folder_path = context.get_current_folder_path() task_name = context.get_current_task_name() host_name = context.host_name if existing_instance is None: - existing_instance_asset = None + existing_instance_folder = None else: - existing_instance_asset = existing_instance["folderPath"] + existing_instance_folder = existing_instance["folderPath"] if existing_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant } data.update(self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, None @@ -83,18 +89,23 @@ class PSAutoCreator(AutoCreator): new_instance.data_to_store()) elif ( - existing_instance_asset != asset_name + existing_instance_folder != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/photoshop/plugins/create/create_flatten_image.py b/client/ayon_core/hosts/photoshop/plugins/create/create_flatten_image.py index 11bf92d5fb..a3bc77c640 100644 --- a/client/ayon_core/hosts/photoshop/plugins/create/create_flatten_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/create/create_flatten_image.py @@ -1,11 +1,9 @@ -from ayon_core.pipeline import CreatedInstance +import ayon_api -from ayon_core.lib import BoolDef import ayon_core.hosts.photoshop.api as api from ayon_core.hosts.photoshop.lib import PSAutoCreator, clean_product_name -from ayon_core.pipeline.create import get_product_name -from ayon_core.lib import prepare_template_data -from ayon_core.client import get_asset_by_name +from ayon_core.lib import BoolDef, prepare_template_data +from ayon_core.pipeline.create import get_product_name, CreatedInstance class AutoImageCreator(PSAutoCreator): @@ -32,27 +30,29 @@ class AutoImageCreator(PSAutoCreator): context = self.create_context project_name = context.get_current_project_name() - asset_name = context.get_current_asset_name() + folder_path = context.get_current_folder_path() task_name = context.get_current_task_name() host_name = context.host_name - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) - if existing_instance is None: - existing_instance_asset = None - else: - existing_instance_asset = existing_instance["folderPath"] + existing_folder_path = None + if existing_instance is not None: + existing_folder_path = existing_instance["folderPath"] if existing_instance is None: product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, } @@ -70,17 +70,17 @@ class AutoImageCreator(PSAutoCreator): new_instance.data_to_store()) elif ( # existing instance from different context - existing_instance_asset != asset_name + existing_folder_path != folder_path or existing_instance["task"] != task_name ): product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name @@ -128,19 +128,26 @@ class AutoImageCreator(PSAutoCreator): def get_product_name( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=None, instance=None ): if host_name is None: host_name = self.create_context.host_name + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + dynamic_data = prepare_template_data({"layer": "{layer}"}) + product_name = get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, self.product_type, variant, diff --git a/client/ayon_core/hosts/photoshop/plugins/create/create_image.py b/client/ayon_core/hosts/photoshop/plugins/create/create_image.py index 8806aad33c..26f2469844 100644 --- a/client/ayon_core/hosts/photoshop/plugins/create/create_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/create/create_image.py @@ -247,8 +247,8 @@ class ImageCreator(Creator): def get_dynamic_data( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, instance diff --git a/client/ayon_core/hosts/photoshop/plugins/load/load_image.py b/client/ayon_core/hosts/photoshop/plugins/load/load_image.py index ec6392bade..42cc2dbcbf 100644 --- a/client/ayon_core/hosts/photoshop/plugins/load/load_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/load/load_image.py @@ -18,7 +18,7 @@ class ImageLoader(photoshop.PhotoshopLoader): stub = self.get_stub() layer_name = get_unique_layer_name( stub.get_layers(), - context["asset"]["name"], + context["folder"]["name"], name ) with photoshop.maintained_selection(): @@ -42,27 +42,29 @@ class ImageLoader(photoshop.PhotoshopLoader): layer = container.pop("layer") - repre_doc = context["representation"] + repre_entity = context["representation"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] namespace_from_container = re.sub(r'_\d{3}$', '', container["namespace"]) - layer_name = "{}_{}".format(context["asset"], context["subset"]) + layer_name = "{}_{}".format(folder_name, product_name) # switching assets if namespace_from_container != layer_name: layer_name = get_unique_layer_name( - stub.get_layers(), context["asset"], context["subset"] + stub.get_layers(), folder_name, product_name ) else: # switching version - keep same name layer_name = container["namespace"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) with photoshop.maintained_selection(): stub.replace_smart_object( layer, path, layer_name ) stub.imprint( - layer.id, {"representation": str(repre_doc["_id"])} + layer.id, {"representation": repre_entity["id"]} ) def remove(self, container): diff --git a/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py b/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py index 49ca513bd2..e20c9a5138 100644 --- a/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py +++ b/client/ayon_core/hosts/photoshop/plugins/load/load_image_from_sequence.py @@ -40,7 +40,7 @@ class ImageFromSequenceLoader(photoshop.PhotoshopLoader): stub = self.get_stub() layer_name = get_unique_layer_name( - stub.get_layers(), context["asset"]["name"], name + stub.get_layers(), context["folder"]["name"], name ) with photoshop.maintained_selection(): diff --git a/client/ayon_core/hosts/photoshop/plugins/load/load_reference.py b/client/ayon_core/hosts/photoshop/plugins/load/load_reference.py index f83272f97d..f1897ad18a 100644 --- a/client/ayon_core/hosts/photoshop/plugins/load/load_reference.py +++ b/client/ayon_core/hosts/photoshop/plugins/load/load_reference.py @@ -20,7 +20,7 @@ class ReferenceLoader(photoshop.PhotoshopLoader): def load(self, context, name=None, namespace=None, data=None): stub = self.get_stub() layer_name = get_unique_layer_name( - stub.get_layers(), context["asset"]["name"], name + stub.get_layers(), context["folder"]["name"], name ) with photoshop.maintained_selection(): path = self.filepath_from_context(context) @@ -38,16 +38,13 @@ class ReferenceLoader(photoshop.PhotoshopLoader): ) def update(self, container, context): - """ Switch asset or change version """ + """ Switch asset or change version.""" stub = self.get_stub() layer = container.pop("layer") - asset_doc = context["asset"] - subset_doc = context["subset"] - repre_doc = context["representation"] - - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + repre_entity = context["representation"] namespace_from_container = re.sub(r'_\d{3}$', '', container["namespace"]) @@ -60,14 +57,14 @@ class ReferenceLoader(photoshop.PhotoshopLoader): else: # switching version - keep same name layer_name = container["namespace"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) with photoshop.maintained_selection(): stub.replace_smart_object( layer, path, layer_name ) stub.imprint( - layer.id, {"representation": str(repre_doc["_id"])} + layer.id, {"representation": repre_entity["id"]} ) def remove(self, container): diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py index 7773b444d2..b488ab364d 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_image.py @@ -1,6 +1,5 @@ import pyblish.api -from ayon_core.client import get_asset_name_identifier from ayon_core.hosts.photoshop import api as photoshop from ayon_core.pipeline.create import get_product_name @@ -25,10 +24,13 @@ class CollectAutoImage(pyblish.api.ContextPlugin): project_name = context.data["projectName"] proj_settings = context.data["project_settings"] - task_name = context.data["task"] host_name = context.data["hostName"] - asset_doc = context.data["assetEntity"] - folder_path = get_asset_name_identifier(asset_doc) + folder_entity = context.data["folderEntity"] + task_entity = context.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] auto_creator = proj_settings.get( "photoshop", {}).get( @@ -81,15 +83,15 @@ class CollectAutoImage(pyblish.api.ContextPlugin): product_name = get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, product_type, variant, ) instance = context.create_instance(product_name) - instance.data["folderPath"] = folder_path + instance.data["folderPath"] = folder_entity["path"] instance.data["productType"] = product_type instance.data["productName"] = product_name instance.data["ids"] = publishable_ids diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_review.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_review.py index 14f2f23985..d7267d253a 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_review.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_review.py @@ -7,7 +7,6 @@ Provides: """ import pyblish.api -from ayon_core.client import get_asset_name_identifier from ayon_core.hosts.photoshop import api as photoshop from ayon_core.pipeline.create import get_product_name @@ -63,16 +62,18 @@ class CollectAutoReview(pyblish.api.ContextPlugin): project_name = context.data["projectName"] proj_settings = context.data["project_settings"] - task_name = context.data["task"] host_name = context.data["hostName"] - asset_doc = context.data["assetEntity"] - - folder_path = get_asset_name_identifier(asset_doc) + folder_entity = context.data["folderEntity"] + task_entity = context.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] product_name = get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, product_type, variant, @@ -88,7 +89,7 @@ class CollectAutoReview(pyblish.api.ContextPlugin): "family": product_type, "families": [product_type], "representations": [], - "folderPath": folder_path, + "folderPath": folder_entity["path"], "publish": self.publish }) diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_workfile.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_workfile.py index 0b12195603..af74c76a15 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_workfile.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_auto_workfile.py @@ -1,7 +1,6 @@ import os import pyblish.api -from ayon_core.client import get_asset_name_identifier from ayon_core.hosts.photoshop import api as photoshop from ayon_core.pipeline.create import get_product_name @@ -69,13 +68,17 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): task_name = context.data["task"] host_name = context.data["hostName"] - asset_doc = context.data["assetEntity"] + folder_entity = context.data["folderEntity"] + task_entity = context.data["taskEntity"] + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] - folder_path = get_asset_name_identifier(asset_doc) product_name = get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, product_type, variant, @@ -92,7 +95,7 @@ class CollectAutoWorkfile(pyblish.api.ContextPlugin): "family": product_type, "families": [product_type], "representations": [], - "folderPath": folder_path + "folderPath": folder_entity["path"] }) # creating representation diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_batch_data.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_batch_data.py index a32b5f8fa5..c43a957576 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_batch_data.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_batch_data.py @@ -18,7 +18,7 @@ import os import pyblish.api from openpype_modules.webpublisher.lib import ( - get_batch_asset_task_info, + get_batch_context_info, parse_json ) from ayon_core.lib import is_in_tests @@ -64,14 +64,14 @@ class CollectBatchData(pyblish.api.ContextPlugin): context.data["batchDir"] = batch_dir context.data["batchData"] = batch_data - asset_name, task_name, task_type = get_batch_asset_task_info( + folder_path, task_name, task_type = get_batch_context_info( batch_data["context"] ) - os.environ["AYON_FOLDER_PATH"] = asset_name + os.environ["AYON_FOLDER_PATH"] = folder_path os.environ["AYON_TASK_NAME"] = task_name - context.data["folderPath"] = asset_name + context.data["folderPath"] = folder_path context.data["task"] = task_name context.data["taskType"] = task_type context.data["project_name"] = project_name diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_color_coded_instances.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_color_coded_instances.py index f11ba4383a..e8f7c7e3df 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_color_coded_instances.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_color_coded_instances.py @@ -57,7 +57,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): existing_product_names = self._get_existing_product_names(context) # from CollectBatchData - asset_name = context.data["folderPath"] + folder_path = context.data["folderPath"] task_name = context.data["task"] variant = context.data["variant"] project_name = context.data["projectEntity"]["name"] @@ -120,7 +120,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): context, layer, resolved_product_type, - asset_name, + folder_path, product_name, task_name ) @@ -146,7 +146,7 @@ class CollectColorCodedInstances(pyblish.api.ContextPlugin): context, first_layer, product_type, - asset_name, + folder_path, product_name, task_name ) diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/collect_published_version.py b/client/ayon_core/hosts/photoshop/plugins/publish/collect_published_version.py index 53b2503ba2..84c9fa3e62 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/collect_published_version.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/collect_published_version.py @@ -9,15 +9,15 @@ product is used instead. This plugin runs only in remote publishing (eg. Webpublisher). Requires: - context.data["assetEntity"] + context.data["folderEntity"] Provides: context["version"] - incremented latest published workfile version """ import pyblish.api +import ayon_api -from ayon_core.client import get_last_version_by_subset_name from ayon_core.pipeline.version_start import get_versioning_start @@ -42,15 +42,14 @@ class CollectPublishedVersion(pyblish.api.ContextPlugin): return project_name = context.data["projectName"] - asset_doc = context.data["assetEntity"] - asset_id = asset_doc["_id"] + folder_id = context.data["folderEntity"]["id"] - version_doc = get_last_version_by_subset_name( - project_name, workfile_product_name, asset_id + version_entity = ayon_api.get_last_version_by_product_name( + project_name, workfile_product_name, folder_id ) - if version_doc: - version_int = int(version_doc["name"]) + 1 + if version_entity: + version_int = int(version_entity["version"]) + 1 else: version_int = get_versioning_start( project_name, diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml b/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml index e05ac92182..c033f922c6 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml +++ b/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_instance_asset.xml @@ -1,9 +1,9 @@ -Asset does not match +Folder does not match -## Collected asset name is not same as in context +## Collected folder path is not same as in context {msg} ### How to repair? @@ -11,10 +11,10 @@ Refresh Publish afterwards (circle arrow at the bottom right). If that's not correct value, close workfile and reopen via Workfiles to get - proper context asset name OR disable this validator and publish again + proper context folder path OR disable this validator and publish again if you are publishing to different context deliberately. - (Context means combination of project, asset name and task name.) + (Context means combination of project, folder path and task name.) \ No newline at end of file diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_naming.xml b/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_naming.xml index 28c2c2c773..28c2329c8a 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_naming.xml +++ b/client/ayon_core/hosts/photoshop/plugins/publish/help/validate_naming.xml @@ -1,11 +1,11 @@ -Subset name +Product name ## Invalid product or layer name -Subset or layer name cannot contain specific characters (spaces etc) which could cause issue when product name is used in a published file name. +Product or layer name cannot contain specific characters (spaces etc) which could cause issue when product name is used in a published file name. {msg} ### How to repair? diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/validate_instance_asset.py b/client/ayon_core/hosts/photoshop/plugins/publish/validate_instance_asset.py index 67a7303316..c3a6822f32 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/validate_instance_asset.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/validate_instance_asset.py @@ -1,6 +1,6 @@ import pyblish.api -from ayon_core.pipeline import get_current_asset_name +from ayon_core.pipeline import get_current_folder_path from ayon_core.pipeline.publish import ( ValidateContentsOrder, PublishXmlValidationError, @@ -9,8 +9,8 @@ from ayon_core.pipeline.publish import ( from ayon_core.hosts.photoshop import api as photoshop -class ValidateInstanceAssetRepair(pyblish.api.Action): - """Repair the instance asset.""" +class ValidateInstanceFolderRepair(pyblish.api.Action): + """Repair the instance folder.""" label = "Repair" icon = "wrench" @@ -21,50 +21,54 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): # Get the errored instances failed = [] for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): + if ( + result["error"] is not None + and result["instance"] is not None + and result["instance"] not in failed + ): failed.append(result["instance"]) # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) stub = photoshop.stub() - current_asset_name = get_current_asset_name() + current_folder_path = get_current_folder_path() for instance in instances: data = stub.read(instance[0]) - data["folderPath"] = current_asset_name + data["folderPath"] = current_folder_path stub.imprint(instance[0], data) class ValidateInstanceAsset(OptionalPyblishPluginMixin, pyblish.api.InstancePlugin): - """Validate the instance asset is the current selected context asset. + """Validate the instance folder is the current selected context folder. As it might happen that multiple worfiles are opened, switching between them would mess with selected context. - In that case outputs might be output under wrong asset! + In that case outputs might be output under wrong folder! - Repair action will use Context asset value (from Workfiles or Launcher) + Repair action will use Context folder value (from Workfiles or Launcher) Closing and reopening with Workfiles will refresh Context value. """ - label = "Validate Instance Asset" + label = "Validate Instance Folder" hosts = ["photoshop"] optional = True - actions = [ValidateInstanceAssetRepair] + actions = [ValidateInstanceFolderRepair] order = ValidateContentsOrder def process(self, instance): - instance_asset = instance.data["folderPath"] - current_asset = get_current_asset_name() + instance_folder_path = instance.data["folderPath"] + current_folder_path = get_current_folder_path() - if instance_asset != current_asset: + if instance_folder_path != current_folder_path: msg = ( - f"Instance asset {instance_asset} is not the same " - f"as current context {current_asset}." + f"Instance folder {instance_folder_path} is not the same" + f" as current context {current_folder_path}." ) repair_msg = ( - f"Repair with 'Repair' button to use '{current_asset}'.\n" + "Repair with 'Repair' button" + f" to use '{current_folder_path}'.\n" ) formatting_data = {"msg": msg, "repair_msg": repair_msg} diff --git a/client/ayon_core/hosts/photoshop/plugins/publish/validate_naming.py b/client/ayon_core/hosts/photoshop/plugins/publish/validate_naming.py index ce940c47ce..13c6a54fd2 100644 --- a/client/ayon_core/hosts/photoshop/plugins/publish/validate_naming.py +++ b/client/ayon_core/hosts/photoshop/plugins/publish/validate_naming.py @@ -11,7 +11,7 @@ from ayon_core.pipeline.publish import ( class ValidateNamingRepair(pyblish.api.Action): - """Repair the instance asset.""" + """Repair the instance folder.""" label = "Repair" icon = "wrench" @@ -22,8 +22,11 @@ class ValidateNamingRepair(pyblish.api.Action): # Get the errored instances failed = [] for result in context.data["results"]: - if (result["error"] is not None and result["instance"] is not None - and result["instance"] not in failed): + if ( + result["error"] is not None + and result["instance"] is not None + and result["instance"] not in failed + ): failed.append(result["instance"]) invalid_chars, replace_char = plugin.get_replace_chars() diff --git a/client/ayon_core/hosts/resolve/api/pipeline.py b/client/ayon_core/hosts/resolve/api/pipeline.py index 19d33971dc..15e4f1203d 100644 --- a/client/ayon_core/hosts/resolve/api/pipeline.py +++ b/client/ayon_core/hosts/resolve/api/pipeline.py @@ -123,7 +123,7 @@ def containerise(timeline_item, "name": str(name), "namespace": str(namespace), "loader": str(loader), - "representation": str(context["representation"]["_id"]), + "representation": context["representation"]["id"], }) if data: diff --git a/client/ayon_core/hosts/resolve/api/plugin.py b/client/ayon_core/hosts/resolve/api/plugin.py index dfce3ea37a..8c97df98b8 100644 --- a/client/ayon_core/hosts/resolve/api/plugin.py +++ b/client/ayon_core/hosts/resolve/api/plugin.py @@ -325,7 +325,7 @@ class ClipLoader: "or call your supervisor") # inject asset data to representation dict - self._get_asset_data() + self._get_folder_attributes() # add active components to class if self.new_timeline: @@ -355,40 +355,41 @@ class ClipLoader: } """ # create name - representation = self.context["representation"] - representation_context = representation["context"] - asset = str(representation_context["asset"]) - product_name = str(representation_context["subset"]) - representation_name = str(representation_context["representation"]) + folder_entity = self.context["folder"] + product_name = self.context["product"]["name"] + repre_entity = self.context["representation"] + + folder_name = folder_entity["name"] + folder_path = folder_entity["path"] + representation_name = repre_entity["name"] + self.data["clip_name"] = "_".join([ - asset, + folder_name, product_name, representation_name ]) - self.data["versionData"] = self.context["version"]["data"] + self.data["versionAttributes"] = self.context["version"]["attrib"] self.data["timeline_basename"] = "timeline_{}_{}".format( product_name, representation_name) # solve project bin structure path - hierarchy = str("/".join(( - "Loader", - representation_context["hierarchy"].replace("\\", "/"), - asset - ))) + hierarchy = "Loader{}".format(folder_path) self.data["binPath"] = hierarchy return True - def _get_asset_data(self): + def _get_folder_attributes(self): """ Get all available asset data joint `data` key with asset.data dict into the representation """ - self.data["assetData"] = copy.deepcopy(self.context["asset"]["data"]) + self.data["folderAttributes"] = copy.deepcopy( + self.context["folder"]["attrib"] + ) def load(self, files): """Load clip into timeline @@ -411,7 +412,7 @@ class ClipLoader: source_duration = int(_clip_property("Frames")) # Trim clip start if slate is present - if "slate" in self.data["versionData"]["families"]: + if "slate" in self.data["versionAttributes"]["families"]: source_in += 1 source_duration = source_out - source_in + 1 @@ -419,13 +420,11 @@ class ClipLoader: # Load file without the handles of the source media # We remove the handles from the source in and source out # so that the handles are excluded in the timeline - handle_start = 0 - handle_end = 0 # get version data frame data from db - version_data = self.data["versionData"] - frame_start = version_data.get("frameStart") - frame_end = version_data.get("frameEnd") + version_attributes = self.data["versionAttributes"] + frame_start = version_attributes.get("frameStart") + frame_end = version_attributes.get("frameEnd") # The version data usually stored the frame range + handles of the # media however certain representations may be shorter because they @@ -437,8 +436,8 @@ class ClipLoader: # from source and out if frame_start is not None and frame_end is not None: # Version has frame range data, so we can compare media length - handle_start = version_data.get("handleStart", 0) - handle_end = version_data.get("handleEnd", 0) + handle_start = version_attributes.get("handleStart", 0) + handle_end = version_attributes.get("handleEnd", 0) frame_start_handle = frame_start - handle_start frame_end_handle = frame_end + handle_end database_frame_duration = int( @@ -456,7 +455,7 @@ class ClipLoader: else: # set timeline start frame + original clip in frame timeline_in = int( - timeline_start + self.data["assetData"]["clipIn"]) + timeline_start + self.data["folderAttributes"]["clipIn"]) # make track item from source in bin as item timeline_item = lib.create_timeline_item( diff --git a/client/ayon_core/hosts/resolve/plugins/load/load_clip.py b/client/ayon_core/hosts/resolve/plugins/load/load_clip.py index 04b2aaaf15..26ee35a90d 100644 --- a/client/ayon_core/hosts/resolve/plugins/load/load_clip.py +++ b/client/ayon_core/hosts/resolve/plugins/load/load_clip.py @@ -1,8 +1,5 @@ -from ayon_core.client import get_last_version_by_subset_id -from ayon_core.pipeline import ( - get_representation_context, - get_current_project_name -) +import ayon_api + from ayon_core.hosts.resolve.api import lib, plugin from ayon_core.hosts.resolve.api.pipeline import ( containerise, @@ -50,7 +47,11 @@ class LoadClip(plugin.TimelineItemLoader): namespace = namespace or timeline_item.GetName() # update color of clip regarding the version order - self.set_item_color(timeline_item, version=context["version"]) + self.set_item_color( + context["project"]["name"], + timeline_item, + context["version"] + ) data_imprint = self.get_tag_data(context, name, namespace) return containerise( @@ -66,20 +67,24 @@ class LoadClip(plugin.TimelineItemLoader): """ Updating previously loaded clips """ - repre_doc = context["representation"] + repre_entity = context["representation"] name = container['name'] namespace = container['namespace'] timeline_item = container["_timeline_item"] media_pool_item = timeline_item.GetMediaPoolItem() - files = plugin.get_representation_files(repre_doc) + files = plugin.get_representation_files(repre_entity) loader = plugin.ClipLoader(self, context) timeline_item = loader.update(timeline_item, files) # update color of clip regarding the version order - self.set_item_color(timeline_item, version=context["version"]) + self.set_item_color( + context["project"]["name"], + timeline_item, + context["version"] + ) # if original media pool item has no remaining usages left # remove it from the media pool @@ -92,11 +97,10 @@ class LoadClip(plugin.TimelineItemLoader): def get_tag_data(self, context, name, namespace): """Return data to be imprinted on the timeline item marker""" - repre_doc = context["representation"] - version_doc = context["version"] - version_data = version_doc.get("data", {}) - version_name = version_doc.get("name", None) - colorspace = version_data.get("colorspace", None) + repre_entity = context["representation"] + version_entity = context["version"] + version_attributes = version_entity["attrib"] + colorspace = version_attributes.get("colorSpace", None) object_name = "{}_{}".format(name, namespace) # add additional metadata from the version to imprint Avalon knob @@ -106,37 +110,34 @@ class LoadClip(plugin.TimelineItemLoader): "fps", "handleStart", "handleEnd" ] data = { - key: version_data.get(key, "None") for key in add_version_data_keys + key: version_attributes.get(key, "None") + for key in add_version_data_keys } # add variables related to version context data.update({ - "representation": str(repre_doc["_id"]), - "version": version_name, + "representation": repre_entity["id"], + "version": version_entity["version"], "colorspace": colorspace, "objectName": object_name }) return data @classmethod - def set_item_color(cls, timeline_item, version): + def set_item_color(cls, project_name, timeline_item, version_entity): """Color timeline item based on whether it is outdated or latest""" - # define version name - version_name = version.get("name", None) # get all versions in list - project_name = get_current_project_name() - last_version_doc = get_last_version_by_subset_id( + last_version_entity = ayon_api.get_last_version_by_product_id( project_name, - version["parent"], + version_entity["productId"], fields=["name"] ) - if last_version_doc: - last_version = last_version_doc["name"] - else: - last_version = None + last_version_id = None + if last_version_entity: + last_version_id = last_version_entity["id"] # set clip colour - if version_name == last_version: + if version_entity["id"] == last_version_id: timeline_item.SetClipColor(cls.clip_color_last) else: timeline_item.SetClipColor(cls.clip_color) diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py index b1374859e3..72ecd3669d 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_instances.py @@ -63,7 +63,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if k not in ("id", "applieswhole", "label") }) - asset = tag_data["folder_path"] + folder_path = tag_data["folder_path"] # TODO: remove backward compatibility product_name = tag_data.get("productName") @@ -74,7 +74,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # backward compatibility: product_name should not be missing if not product_name: self.log.error( - "Product name is not defined for: {}".format(asset)) + "Product name is not defined for: {}".format(folder_path)) # TODO: remove backward compatibility product_type = tag_data.get("productType") @@ -85,12 +85,12 @@ class PrecollectInstances(pyblish.api.ContextPlugin): # backward compatibility: product_type should not be missing if not product_type: self.log.error( - "Product type is not defined for: {}".format(asset)) + "Product type is not defined for: {}".format(folder_path)) data.update({ - "name": "{}_{}".format(asset, product_name), - "label": "{} {}".format(asset, product_name), - "folderPath": asset, + "name": "{}_{}".format(folder_path, product_name), + "label": "{} {}".format(folder_path, product_name), + "folderPath": folder_path, "item": timeline_item, "publish": get_publish_attribute(timeline_item), "fps": context.data["fps"], @@ -151,16 +151,16 @@ class PrecollectInstances(pyblish.api.ContextPlugin): if not hierarchy_data: return - asset = data["folderPath"] + folder_path = data["folderPath"] product_name = "shotMain" # insert family into families product_type = "shot" data.update({ - "name": "{}_{}".format(asset, product_name), - "label": "{} {}".format(asset, product_name), - "folderPath": asset, + "name": "{}_{}".format(folder_path, product_name), + "label": "{} {}".format(folder_path, product_name), + "folderPath": folder_path, "productName": product_name, "productType": product_type, "family": product_type, diff --git a/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py b/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py index a147c9a905..6158cf1d61 100644 --- a/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/client/ayon_core/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,7 +1,7 @@ import pyblish.api from pprint import pformat -from ayon_core.pipeline import get_current_asset_name +from ayon_core.pipeline import get_current_folder_path from ayon_core.hosts.resolve import api as rapi from ayon_core.hosts.resolve.otio import davinci_export @@ -14,8 +14,8 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder - 0.5 def process(self, context): - current_asset_name = get_current_asset_name() - asset_name = current_asset_name.split("/")[-1] + current_folder_path = get_current_folder_path() + folder_name = current_folder_path.split("/")[-1] product_name = "workfileMain" project = rapi.get_current_project() @@ -26,10 +26,10 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): otio_timeline = davinci_export.create_otio_timeline(project) instance_data = { - "name": "{}_{}".format(asset_name, product_name), - "label": "{} {}".format(current_asset_name, product_name), + "name": "{}_{}".format(folder_name, product_name), + "label": "{} {}".format(current_folder_path, product_name), "item": project, - "folderPath": current_asset_name, + "folderPath": current_folder_path, "productName": product_name, "productType": "workfile", "family": "workfile", diff --git a/client/ayon_core/hosts/substancepainter/api/pipeline.py b/client/ayon_core/hosts/substancepainter/api/pipeline.py index 843c120d8e..c75cc3135a 100644 --- a/client/ayon_core/hosts/substancepainter/api/pipeline.py +++ b/client/ayon_core/hosts/substancepainter/api/pipeline.py @@ -245,16 +245,15 @@ class SubstanceHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return # Prepare formatting data if we detect any path which might have - # template tokens like {asset} in there. + # template tokens like {folder[name]} in there. formatting_data = {} has_formatting_entries = any("{" in item["value"] for item in shelves) if has_formatting_entries: project_name = self.get_current_project_name() - asset_name = self.get_current_asset_name() - task_name = self.get_current_asset_name() - project_settings = get_project_settings(project_name) + folder_path = self.get_current_folder_path() + task_name = self.get_current_task_name() formatting_data = get_template_data_with_names( - project_name, asset_name, task_name, project_settings + project_name, folder_path, task_name, project_settings ) anatomy = Anatomy(project_name) formatting_data["root"] = anatomy.roots @@ -338,7 +337,7 @@ def imprint_container(container, ("name", str(name)), ("namespace", str(namespace) if namespace else None), ("loader", str(loader.__class__.__name__)), - ("representation", str(context["representation"]["_id"])), + ("representation", context["representation"]["id"]), ] for key, value in data: container[key] = value diff --git a/client/ayon_core/hosts/substancepainter/plugins/create/create_workfile.py b/client/ayon_core/hosts/substancepainter/plugins/create/create_workfile.py index 23811dfd29..63b1c6c7da 100644 --- a/client/ayon_core/hosts/substancepainter/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/substancepainter/plugins/create/create_workfile.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- """Creator plugin for creating workfiles.""" +import ayon_api + from ayon_core.pipeline import CreatedInstance, AutoCreator -from ayon_core.client import get_asset_by_name from ayon_core.hosts.substancepainter.api.pipeline import ( set_instances, @@ -29,7 +30,7 @@ class CreateWorkfile(AutoCreator): variant = self.default_variant project_name = self.project_name - asset_name = self.create_context.get_current_asset_name() + folder_path = self.create_context.get_current_folder_path() task_name = self.create_context.get_current_task_name() host_name = self.create_context.host_name @@ -41,42 +42,51 @@ class CreateWorkfile(AutoCreator): if instance.creator_identifier == self.identifier ), None) - if current_instance is None: - current_instance_asset = None - else: - current_instance_asset = current_instance["folderPath"] + current_folder_path = None + if current_instance is not None: + current_folder_path = current_instance["folderPath"] if current_instance is None: self.log.info("Auto-creating workfile instance...") - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": variant } current_instance = self.create_instance_in_context(product_name, data) elif ( - current_instance_asset != asset_name + current_folder_path != folder_path or current_instance["task"] != task_name ): # Update instance context if is not the same - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, ) - current_instance["folderPath"] = asset_name + current_instance["folderPath"] = folder_path current_instance["task"] = task_name current_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py b/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py index 810fecb8e5..fb2f1db726 100644 --- a/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py +++ b/client/ayon_core/hosts/substancepainter/plugins/load/load_mesh.py @@ -101,9 +101,9 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): self.update(container, context) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) # Reload the mesh container_options = container.get("options", {}) @@ -122,7 +122,7 @@ class SubstanceLoadProjectMesh(load.LoaderPlugin): # Update container representation object_name = container["objectName"] - update_data = {"representation": str(repre_doc["_id"])} + update_data = {"representation": repre_entity["id"]} set_container_metadata(object_name, update_data, update=True) def remove(self, container): diff --git a/client/ayon_core/hosts/substancepainter/plugins/publish/collect_textureset_images.py b/client/ayon_core/hosts/substancepainter/plugins/publish/collect_textureset_images.py index 9cd77e8f90..20aaa56993 100644 --- a/client/ayon_core/hosts/substancepainter/plugins/publish/collect_textureset_images.py +++ b/client/ayon_core/hosts/substancepainter/plugins/publish/collect_textureset_images.py @@ -1,16 +1,16 @@ import os import copy -import pyblish.api -from ayon_core.pipeline import publish +import pyblish.api +import ayon_api import substance_painter.textureset +from ayon_core.pipeline import publish from ayon_core.hosts.substancepainter.api.lib import ( get_parsed_export_maps, strip_template ) from ayon_core.pipeline.create import get_product_name -from ayon_core.client import get_asset_by_name class CollectTextureSet(pyblish.api.InstancePlugin): @@ -26,10 +26,17 @@ class CollectTextureSet(pyblish.api.InstancePlugin): def process(self, instance): config = self.get_export_config(instance) - asset_doc = get_asset_by_name( - instance.context.data["projectName"], + project_name = instance.context.data["projectName"] + folder_entity = ayon_api.get_folder_by_path( + project_name, instance.data["folderPath"] ) + task_name = instance.data.get("task") + task_entity = None + if folder_entity and task_name: + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) instance.data["exportConfig"] = config maps = get_parsed_export_maps(config) @@ -41,12 +48,12 @@ class CollectTextureSet(pyblish.api.InstancePlugin): for template, outputs in template_maps.items(): self.log.info(f"Processing {template}") self.create_image_instance(instance, template, outputs, - asset_doc=asset_doc, + task_entity=task_entity, texture_set_name=texture_set_name, stack_name=stack_name) def create_image_instance(self, instance, template, outputs, - asset_doc, texture_set_name, stack_name): + task_entity, texture_set_name, stack_name): """Create a new instance per image or UDIM sequence. The new instances will be of product type `image`. @@ -79,14 +86,19 @@ class CollectTextureSet(pyblish.api.InstancePlugin): map_identifier = strip_template(template) suffix += f".{map_identifier}" + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + image_product_name = get_product_name( # TODO: The product type actually isn't 'texture' currently but # for now this is only done so the product name starts with # 'texture' - project_name=context.data["projectName"], - asset_doc=asset_doc, - task_name=instance.data.get("task"), - host_name=context.data["hostName"], + context.data["projectName"], + task_name, + task_type, + context.data["hostName"], product_type="texture", variant=instance.data["variant"] + suffix, project_settings=context.data["project_settings"] @@ -126,7 +138,7 @@ class CollectTextureSet(pyblish.api.InstancePlugin): image_instance.data["representations"] = [representation] # Group the textures together in the loader - image_instance.data["subsetGroup"] = image_product_name + image_instance.data["productGroup"] = image_product_name # Store the texture set name and stack name on the instance image_instance.data["textureSetName"] = texture_set_name diff --git a/client/ayon_core/hosts/traypublisher/api/editorial.py b/client/ayon_core/hosts/traypublisher/api/editorial.py index 6153bc5752..8dedec7398 100644 --- a/client/ayon_core/hosts/traypublisher/api/editorial.py +++ b/client/ayon_core/hosts/traypublisher/api/editorial.py @@ -1,7 +1,8 @@ import re from copy import deepcopy -from ayon_core.client import get_asset_by_id +import ayon_api + from ayon_core.pipeline.create import CreatorError @@ -88,7 +89,7 @@ class ShotMetadataSolver: if not self.clip_name_tokenizer: return output_data - parent_name = source_data["selected_asset_doc"]["name"] + parent_name = source_data["selected_folder_entity"]["name"] search_text = parent_name + clip_name @@ -157,11 +158,11 @@ class ShotMetadataSolver: **_parent_tokens_formatting_data) except KeyError as _error: raise CreatorError(( - "Make sure all keys in settings are correct : \n\n" - f"`{_error}` from template string " - f"{shot_hierarchy['parents_path']}, " - f" has no equivalent in \n" - f"{list(_parent_tokens_formatting_data.keys())} parents" + "Make sure all keys in settings are correct:\n\n" + f"`{_error}` from template string" + f" {shot_hierarchy['parents_path']}," + f" has no equivalent in" + f"\n{list(_parent_tokens_formatting_data.keys())} parents" )) parent_token_name = ( @@ -174,7 +175,8 @@ class ShotMetadataSolver: # find parent type parent_token_type = _parent_tokens_type[parent_token_name] - # in case selected context is set to the same asset + # in case selected context is set to the same folder + # TODO keep index with 'parents' - name check is not enough if ( _index == 0 and parents[-1]["entity_name"] == parent_name @@ -184,7 +186,7 @@ class ShotMetadataSolver: # in case first parent is project then start parents from start if ( _index == 0 - and parent_token_type == "Project" + and parent_token_type == "project" ): project_parent = parents[0] parents = [project_parent] @@ -209,58 +211,69 @@ class ShotMetadataSolver: return "/".join( [ p["entity_name"] for p in parents - if p["entity_type"] != "Project" + if p["entity_type"] != "project" ] ) if parents else "" - def _get_parents_from_selected_asset( + def _get_parents_from_selected_folder( self, - asset_doc, - project_doc + project_entity, + folder_entity, ): - """Returning parents from context on selected asset. + """Returning parents from context on selected folder. Context defined in Traypublisher project tree. Args: - asset_doc (db obj): selected asset doc - project_doc (db obj): actual project doc + project_entity (dict[str, Any]): Project entity. + folder_entity (dict[str, Any]): Selected folder entity. Returns: - list: list of dict parent components + list: list of dict parent components """ - project_name = project_doc["name"] - visual_hierarchy = [asset_doc] - current_doc = asset_doc - # looping through all available visual parents - # if they are not available anymore than it breaks - while True: - visual_parent_id = current_doc["data"]["visualParent"] - visual_parent = None - if visual_parent_id: - visual_parent = get_asset_by_id(project_name, visual_parent_id) + project_name = project_entity["name"] + path_entries = folder_entity["path"].split("/") + subpaths = [] + subpath_items = [] + for name in path_entries: + subpath_items.append(name) + if name: + subpaths.append("/".join(subpath_items)) + # Remove last name because we already have folder entity + subpaths.pop(-1) - if not visual_parent: - visual_hierarchy.append(project_doc) - break - visual_hierarchy.append(visual_parent) - current_doc = visual_parent + folder_entity_by_path = {} + if subpaths: + folder_entity_by_path = { + parent_folder["path"]: parent_folder + for parent_folder in ayon_api.get_folders( + project_name, folder_paths=subpaths + ) + } + folders_hierarchy = [ + folder_entity_by_path[folder_path] + for folder_path in subpaths + ] + folders_hierarchy.append(folder_entity) # add current selection context hierarchy - return [ - { - "entity_type": entity["data"]["entityType"], + output = [{ + "entity_type": "project", + "entity_name": project_name, + }] + for entity in folders_hierarchy: + output.append({ + "entity_type": entity["folderType"], "entity_name": entity["name"] - } - for entity in reversed(visual_hierarchy) - ] + }) + return output - def _generate_tasks_from_settings(self, project_doc): + def _generate_tasks_from_settings(self, project_entity): """Convert settings inputs to task data. Args: - project_doc (db obj): actual project doc + project_entity (dict): Project entity. Raises: KeyError: Missing task type in project doc @@ -270,19 +283,23 @@ class ShotMetadataSolver: """ tasks_to_add = {} - project_task_types = project_doc["config"]["tasks"] + project_task_types = project_entity["taskTypes"] + task_type_names = { + task_type["name"] + for task_type in project_task_types + } for task_item in self.shot_add_tasks: task_name = task_item["name"] task_type = task_item["task_type"] # check if task type in project task types - if task_type not in project_task_types.keys(): + if task_type not in task_type_names: raise KeyError( "Missing task type `{}` for `{}` is not" " existing in `{}``".format( task_type, task_name, - list(project_task_types.keys()) + list(task_type_names) ) ) tasks_to_add[task_name] = {"type": task_type} @@ -303,8 +320,8 @@ class ShotMetadataSolver: """ tasks = {} - asset_doc = source_data["selected_asset_doc"] - project_doc = source_data["project_doc"] + folder_entity = source_data["selected_folder_entity"] + project_entity = source_data["project_entity"] # match clip to shot name at start shot_name = clip_name @@ -312,8 +329,10 @@ class ShotMetadataSolver: # parse all tokens and generate formatting data formatting_data = self._generate_tokens(shot_name, source_data) - # generate parents from selected asset - parents = self._get_parents_from_selected_asset(asset_doc, project_doc) + # generate parents from selected folder + parents = self._get_parents_from_selected_folder( + project_entity, folder_entity + ) if self.shot_rename["enabled"]: shot_name = self._rename_template(formatting_data) @@ -325,7 +344,7 @@ class ShotMetadataSolver: if self.shot_add_tasks: tasks = self._generate_tasks_from_settings( - project_doc) + project_entity) # generate hierarchy path from parents hierarchy_path = self._create_hierarchy_path(parents) diff --git a/client/ayon_core/hosts/traypublisher/api/plugin.py b/client/ayon_core/hosts/traypublisher/api/plugin.py index be50383510..257d01eb50 100644 --- a/client/ayon_core/hosts/traypublisher/api/plugin.py +++ b/client/ayon_core/hosts/traypublisher/api/plugin.py @@ -1,9 +1,5 @@ -from ayon_core.client import ( - get_assets, - get_subsets, - get_last_versions, - get_asset_name_identifier, -) +import ayon_api + from ayon_core.lib.attribute_definitions import ( FileDef, BoolDef, @@ -117,10 +113,12 @@ class SettingsCreator(TrayPublishCreator): # Fill 'version_to_use' if version control is enabled if self.allow_version_control: - asset_name = data["folderPath"] - subset_docs_by_asset_id = self._prepare_next_versions( - [asset_name], [product_name]) - version = subset_docs_by_asset_id[asset_name].get(product_name) + folder_path = data["folderPath"] + product_entities_by_folder_path = self._prepare_next_versions( + [folder_path], [product_name]) + version = product_entities_by_folder_path[folder_path].get( + product_name + ) pre_create_data["version_to_use"] = version data["_previous_last_version"] = version @@ -137,67 +135,69 @@ class SettingsCreator(TrayPublishCreator): if thumbnail_path: self.set_instance_thumbnail_path(new_instance.id, thumbnail_path) - def _prepare_next_versions(self, asset_names, product_names): - """Prepare next versions for given asset and product names. + def _prepare_next_versions(self, folder_paths, product_names): + """Prepare next versions for given folder and product names. Todos: - Expect combination of product names by asset name to avoid + Expect combination of product names by folder path to avoid unnecessary server calls for unused products. Args: - asset_names (Iterable[str]): Asset names. - product_names (Iterable[str]): Subset names. + folder_paths (Iterable[str]): Folder paths. + product_names (Iterable[str]): Product names. Returns: - dict[str, dict[str, int]]: Last versions by asset + dict[str, dict[str, int]]: Last versions by fodler path and product names. """ # Prepare all versions for all combinations to '1' # TODO use 'ayon_core.pipeline.version_start' logic - subset_docs_by_asset_id = { - asset_name: { + product_entities_by_folder_path = { + folder_path: { product_name: 1 for product_name in product_names } - for asset_name in asset_names + for folder_path in folder_paths } - if not asset_names or not product_names: - return subset_docs_by_asset_id + if not folder_paths or not product_names: + return product_entities_by_folder_path - asset_docs = get_assets( + folder_entities = ayon_api.get_folders( self.project_name, - asset_names=asset_names, - fields=["_id", "name", "data.parents"] + folder_paths=folder_paths, + fields={"id", "path"} ) - asset_names_by_id = { - asset_doc["_id"]: get_asset_name_identifier(asset_doc) - for asset_doc in asset_docs + folder_paths_by_id = { + folder_entity["id"]: folder_entity["path"] + for folder_entity in folder_entities } - subset_docs = list(get_subsets( + product_entities = list(ayon_api.get_products( self.project_name, - asset_ids=asset_names_by_id.keys(), - subset_names=product_names, - fields=["_id", "name", "parent"] + folder_ids=folder_paths_by_id.keys(), + product_names=product_names, + fields={"id", "name", "folderId"} )) - product_ids = {subset_doc["_id"] for subset_doc in subset_docs} - last_versions = get_last_versions( + product_ids = {p["id"] for p in product_entities} + last_versions = ayon_api.get_last_versions( self.project_name, product_ids, - fields=["name", "parent"]) + fields={"version", "productId"}) - for subset_doc in subset_docs: - asset_id = subset_doc["parent"] - asset_name = asset_names_by_id[asset_id] - product_name = subset_doc["name"] - product_id = subset_doc["_id"] + for product_entity in product_entities: + product_id = product_entity["id"] + product_name = product_entity["name"] + folder_id = product_entity["folderId"] + folder_path = folder_paths_by_id[folder_id] last_version = last_versions.get(product_id) version = 0 if last_version is not None: - version = last_version["name"] - subset_docs_by_asset_id[asset_name][product_name] += version - return subset_docs_by_asset_id + version = last_version["version"] + product_entities_by_folder_path[folder_path][product_name] += ( + version + ) + return product_entities_by_folder_path def _fill_next_versions(self, instances_data): """Fill next version for instances. @@ -222,20 +222,20 @@ class SettingsCreator(TrayPublishCreator): ): filtered_instance_data.append(instance) - asset_names = { + folder_paths = { instance["folderPath"] for instance in filtered_instance_data } product_names = { instance["productName"] for instance in filtered_instance_data} - subset_docs_by_asset_id = self._prepare_next_versions( - asset_names, product_names + product_entities_by_folder_path = self._prepare_next_versions( + folder_paths, product_names ) for instance in filtered_instance_data: - asset_name = instance["folderPath"] + folder_path = instance["folderPath"] product_name = instance["productName"] - version = subset_docs_by_asset_id[asset_name][product_name] + version = product_entities_by_folder_path[folder_path][product_name] instance["creator_attributes"]["version_to_use"] = version instance["_previous_last_version"] = version diff --git a/client/ayon_core/hosts/traypublisher/batch_parsing.py b/client/ayon_core/hosts/traypublisher/batch_parsing.py index fdb3021a20..fa3c8d5b9a 100644 --- a/client/ayon_core/hosts/traypublisher/batch_parsing.py +++ b/client/ayon_core/hosts/traypublisher/batch_parsing.py @@ -2,13 +2,18 @@ import os import re +import ayon_api + from ayon_core.lib import Logger -from ayon_core.client import get_assets, get_asset_by_name -def get_asset_doc_from_file_name(source_filename, project_name, - version_regex, all_selected_asset_ids=None): - """Try to parse out asset name from file name provided. +def get_folder_entity_from_filename( + project_name, + source_filename, + version_regex, + all_selected_folder_ids=None +): + """Try to parse out folder name from file name provided. Artists might provide various file name formats. Currently handled: @@ -17,72 +22,101 @@ def get_asset_doc_from_file_name(source_filename, project_name, - my_chair_to_upload.mov """ version = None - asset_name = os.path.splitext(source_filename)[0] - # Always first check if source filename is directly asset (eg. 'chair.mov') - matching_asset_doc = get_asset_by_name_case_not_sensitive( - project_name, asset_name, all_selected_asset_ids) + folder_name = os.path.splitext(source_filename)[0] + # Always first check if source filename is directly folder + # (eg. 'chair.mov') + matching_folder_entity = get_folder_by_name_case_not_sensitive( + project_name, folder_name, all_selected_folder_ids) - if matching_asset_doc is None: + if matching_folder_entity is None: # name contains also a version - matching_asset_doc, version = ( - parse_with_version(project_name, asset_name, version_regex, - all_selected_asset_ids)) + matching_folder_entity, version = ( + parse_with_version( + project_name, + folder_name, + version_regex, + all_selected_folder_ids + ) + ) - if matching_asset_doc is None: - matching_asset_doc = parse_containing(project_name, asset_name, - all_selected_asset_ids) + if matching_folder_entity is None: + matching_folder_entity = parse_containing( + project_name, + folder_name, + all_selected_folder_ids + ) - return matching_asset_doc, version + return matching_folder_entity, version -def parse_with_version(project_name, asset_name, version_regex, - all_selected_asset_ids=None, log=None): - """Try to parse asset name from a file name containing version too +def parse_with_version( + project_name, + folder_name, + version_regex, + all_selected_folder_ids=None, + log=None +): + """Try to parse folder name from a file name containing version too Eg. 'chair_v001.mov' >> 'chair', 1 """ if not log: log = Logger.get_logger(__name__) log.debug( - ("Asset doc by \"{}\" was not found, trying version regex.". - format(asset_name))) + ("Folder entity by \"{}\" was not found, trying version regex.". + format(folder_name))) - matching_asset_doc = version_number = None + matching_folder_entity = version_number = None - regex_result = version_regex.findall(asset_name) + regex_result = version_regex.findall(folder_name) if regex_result: - _asset_name, _version_number = regex_result[0] - matching_asset_doc = get_asset_by_name_case_not_sensitive( - project_name, _asset_name, - all_selected_asset_ids=all_selected_asset_ids) - if matching_asset_doc: + _folder_name, _version_number = regex_result[0] + matching_folder_entity = get_folder_by_name_case_not_sensitive( + project_name, + _folder_name, + all_selected_folder_ids=all_selected_folder_ids + ) + if matching_folder_entity: version_number = int(_version_number) - return matching_asset_doc, version_number + return matching_folder_entity, version_number -def parse_containing(project_name, asset_name, all_selected_asset_ids=None): - """Look if file name contains any existing asset name""" - for asset_doc in get_assets(project_name, asset_ids=all_selected_asset_ids, - fields=["name"]): - if asset_doc["name"].lower() in asset_name.lower(): - return get_asset_by_name(project_name, asset_doc["name"]) +def parse_containing(project_name, folder_name, all_selected_folder_ids=None): + """Look if file name contains any existing folder name""" + for folder_entity in ayon_api.get_folders( + project_name, + folder_ids=all_selected_folder_ids, + fields={"id", "name"} + ): + if folder_entity["name"].lower() in folder_name.lower(): + return ayon_api.get_folder_by_id( + project_name, + folder_entity["id"] + ) -def get_asset_by_name_case_not_sensitive(project_name, asset_name, - all_selected_asset_ids=None, - log=None): +def get_folder_by_name_case_not_sensitive( + project_name, + folder_name, + all_selected_folder_ids=None, + log=None +): """Handle more cases in file names""" if not log: log = Logger.get_logger(__name__) - asset_name = re.compile(asset_name, re.IGNORECASE) + folder_name = re.compile(folder_name, re.IGNORECASE) - assets = list(get_assets(project_name, asset_ids=all_selected_asset_ids, - asset_names=[asset_name])) - if assets: - if len(assets) > 1: - log.warning("Too many records found for {}".format( - asset_name)) - return + folder_entities = list(ayon_api.get_folders( + project_name, + folder_ids=all_selected_folder_ids, + folder_names=[folder_name] + )) - return assets.pop() + if len(folder_entities) > 1: + log.warning("Too many records found for {}".format( + folder_name)) + return None + + if folder_entities: + return folder_entities.pop() diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py index 5c913b3289..4d865c1c5c 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_colorspace_look.py @@ -5,8 +5,8 @@ This creator is used to publish colorspace look files thanks to production type `ociolook`. All files are published as representation. """ from pathlib import Path +import ayon_api -from ayon_core.client import get_asset_by_name from ayon_core.lib.attribute_definitions import ( FileDef, EnumDef, TextDef, UISeparatorDef ) @@ -54,14 +54,21 @@ This creator publishes color space look file (LUT). # this should never happen raise CreatorError("Missing files from representation") - asset_name = instance_data["folderPath"] - asset_doc = get_asset_by_name( - self.project_name, asset_name) + folder_path = instance_data["folderPath"] + task_name = instance_data["task"] + folder_entity = ayon_api.get_folder_by_path( + self.project_name, folder_path) + + task_entity = None + if task_name: + task_entity = ayon_api.get_task_by_name( + self.project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name=self.project_name, - asset_doc=asset_doc, - task_name=instance_data["task"] or "Not set", + folder_entity=folder_entity, + task_entity=task_entity, variant=instance_data["variant"], ) diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py index a7abd3e6db..a9ee343dfb 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_editorial.py @@ -1,11 +1,9 @@ import os from copy import deepcopy + +import ayon_api import opentimelineio as otio -from ayon_core.client import ( - get_asset_by_name, - get_project -) from ayon_core.hosts.traypublisher.api.plugin import ( TrayPublishCreator, HiddenTrayPublishCreator @@ -196,12 +194,14 @@ or updating already created. Publishing will create OTIO file. if k not in product_types } - asset_name = instance_data["folderPath"] - asset_doc = get_asset_by_name(self.project_name, asset_name) + folder_path = instance_data["folderPath"] + folder_entity = ayon_api.get_folder_by_path( + self.project_name, folder_path + ) if pre_create_data["fps"] == "from_selection": - # get asset doc data attributes - fps = asset_doc["data"]["fps"] + # get 'fps' from folder attributes + fps = folder_entity["attrib"]["fps"] else: fps = float(pre_create_data["fps"]) @@ -226,18 +226,18 @@ or updating already created. Publishing will create OTIO file. # Create all clip instances clip_instance_properties.update({ "fps": fps, - "parent_asset_name": asset_name, "variant": instance_data["variant"] }) # create clip instances self._get_clip_instances( + folder_entity, otio_timeline, media_path, clip_instance_properties, allowed_product_type_presets, os.path.basename(seq_path), - first_otio_timeline + first_otio_timeline, ) if not first_otio_timeline: @@ -248,7 +248,8 @@ or updating already created. Publishing will create OTIO file. self._create_otio_instance( product_name, instance_data, - seq_path, media_path, + seq_path, + media_path, first_otio_timeline ) @@ -332,6 +333,7 @@ or updating already created. Publishing will create OTIO file. def _get_clip_instances( self, + folder_entity, otio_timeline, media_path, instance_data, @@ -342,6 +344,7 @@ or updating already created. Publishing will create OTIO file. """Helping function for creating clip instance Args: + folder_entity (dict[str, Any]): Folder entity. otio_timeline (otio.Timeline): otio timeline object media_path (str): media file path string instance_data (dict): clip instance data @@ -373,7 +376,6 @@ or updating already created. Publishing will create OTIO file. if not self._validate_clip_for_processing(otio_clip): continue - # get available frames info to clip data self._create_otio_reference(otio_clip, media_path, media_data) @@ -383,7 +385,8 @@ or updating already created. Publishing will create OTIO file. base_instance_data = self._get_base_instance_data( otio_clip, instance_data, - track_start_frame + track_start_frame, + folder_entity ) parenting_data = { @@ -566,7 +569,7 @@ or updating already created. Publishing will create OTIO file. return c_instance def _make_product_naming(self, product_type_preset, instance_data): - """Subset name maker + """Product name maker Args: product_type_preset (dict): single preset item @@ -575,7 +578,7 @@ or updating already created. Publishing will create OTIO file. Returns: str: label string """ - asset_name = instance_data["creator_attributes"]["folderPath"] + folder_path = instance_data["creator_attributes"]["folderPath"] variant_name = instance_data["variant"] product_type = product_type_preset["product_type"] @@ -588,7 +591,7 @@ or updating already created. Publishing will create OTIO file. product_type, _variant_name.capitalize() ) label = "{} {}".format( - asset_name, + folder_path, product_name ) @@ -606,6 +609,7 @@ or updating already created. Publishing will create OTIO file. otio_clip, instance_data, track_start_frame, + folder_entity, ): """Factoring basic set of instance data. @@ -616,9 +620,12 @@ or updating already created. Publishing will create OTIO file. Returns: dict: instance data + """ + parent_folder_path = folder_entity["path"] + parent_folder_name = parent_folder_path.rsplit("/", 1)[-1] + # get clip instance properties - parent_asset_name = instance_data["parent_asset_name"] handle_start = instance_data["handle_start"] handle_end = instance_data["handle_end"] timeline_offset = instance_data["timeline_offset"] @@ -626,9 +633,9 @@ or updating already created. Publishing will create OTIO file. fps = instance_data["fps"] variant_name = instance_data["variant"] - # basic unique asset name + # basic unique folder name clip_name = os.path.splitext(otio_clip.name)[0] - project_doc = get_project(self.project_name) + project_entity = ayon_api.get_project(self.project_name) shot_name, shot_metadata = self._shot_metadata_solver.generate_data( clip_name, @@ -636,14 +643,13 @@ or updating already created. Publishing will create OTIO file. "anatomy_data": { "project": { "name": self.project_name, - "code": project_doc["data"]["code"] + "code": project_entity["code"] }, - "parent": parent_asset_name, + "parent": parent_folder_name, "app": self.host_name }, - "selected_asset_doc": get_asset_by_name( - self.project_name, parent_asset_name), - "project_doc": project_doc + "selected_folder_entity": folder_entity, + "project_entity": project_entity } ) @@ -680,7 +686,7 @@ or updating already created. Publishing will create OTIO file. # update base instance data with context data # and also update creator attributes with context data creator_attributes["folderPath"] = shot_metadata.pop("folderPath") - base_instance_data["folderPath"] = parent_asset_name + base_instance_data["folderPath"] = parent_folder_path # add creator attributes to shared instance data base_instance_data["creator_attributes"] = creator_attributes diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py index 9b3dfdd334..546408b4d6 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_movie_batch.py @@ -1,8 +1,10 @@ import copy import os import re +import collections + +import ayon_api -from ayon_core.client import get_asset_name_identifier from ayon_core.lib import ( FileDef, BoolDef, @@ -17,7 +19,7 @@ from ayon_core.pipeline.create import ( from ayon_core.hosts.traypublisher.api.plugin import TrayPublishCreator from ayon_core.hosts.traypublisher.batch_parsing import ( - get_asset_doc_from_file_name + get_folder_entity_from_filename ) @@ -53,21 +55,46 @@ class BatchMovieCreator(TrayPublishCreator): if not file_paths: return + data_by_folder_id = collections.defaultdict(list) for file_info in file_paths: instance_data = copy.deepcopy(data) file_name = file_info["filenames"][0] filepath = os.path.join(file_info["directory"], file_name) instance_data["creator_attributes"] = {"filepath": filepath} - asset_doc, version = get_asset_doc_from_file_name( - file_name, self.project_name, self.version_regex) + folder_entity, version = get_folder_entity_from_filename( + self.project_name, file_name, self.version_regex) + data_by_folder_id[folder_entity["id"]].append( + (instance_data, folder_entity) + ) - product_name, task_name = self._get_product_and_task( - asset_doc, data["variant"], self.project_name) + all_task_entities = ayon_api.get_tasks( + self.project_name, task_ids=set(data_by_folder_id.keys()) + ) + task_entity_by_folder_id = collections.defaultdict(dict) + for task_entity in all_task_entities: + folder_id = task_entity["folderId"] + task_name = task_entity["name"].lower() + task_entity_by_folder_id[folder_id][task_name] = task_entity - asset_name = get_asset_name_identifier(asset_doc) + for ( + folder_id, (instance_data, folder_entity) + ) in data_by_folder_id.items(): + task_entities_by_name = task_entity_by_folder_id[folder_id] + task_name = None + task_entity = None + for default_task_name in self.default_tasks: + _name = default_task_name.lower() + if _name in task_entities_by_name: + task_name = task_entity["name"] + task_entity = task_entities_by_name[_name] + break - instance_data["folderPath"] = asset_name + product_name = self._get_product_name( + self.project_name, task_entity, data["variant"] + ) + + instance_data["folderPath"] = folder_entity["path"] instance_data["task"] = task_name # Create new instance @@ -75,15 +102,18 @@ class BatchMovieCreator(TrayPublishCreator): instance_data, self) self._store_new_instance(new_instance) - def _get_product_and_task(self, asset_doc, variant, project_name): + def _get_product_name(self, project_name, task_entity, variant): """Create product name according to standard template process""" - task_name = self._get_task_name(asset_doc) host_name = self.create_context.host_name + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] try: product_name = get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, self.product_type, variant, @@ -92,34 +122,18 @@ class BatchMovieCreator(TrayPublishCreator): # Create instance with fake task # - instance will be marked as invalid so it can't be published # but user have ability to change it - # NOTE: This expect that there is not task 'Undefined' on asset - task_name = "Undefined" + # NOTE: This expect that there is not task 'Undefined' on folder + dumb_value = "Undefined" product_name = get_product_name( project_name, - asset_doc, - task_name, + dumb_value, + dumb_value, host_name, self.product_type, variant, ) - return product_name, task_name - - def _get_task_name(self, asset_doc): - """Get applicable task from 'asset_doc' """ - available_task_names = {} - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - for task_name in asset_tasks.keys(): - available_task_names[task_name.lower()] = task_name - - task_name = None - for _task_name in self.default_tasks: - _task_name_low = _task_name.lower() - if _task_name_low in available_task_names: - task_name = available_task_names[_task_name_low] - break - - return task_name + return product_name def get_instance_attr_defs(self): return [ @@ -149,8 +163,8 @@ class BatchMovieCreator(TrayPublishCreator): ] def get_detail_description(self): - return """# Publish batch of .mov to multiple assets. + return """# Publish batch of .mov to multiple folders. - File names must then contain only asset name, or asset name + version. + File names must then contain only folder name, or folder name + version. (eg. 'chair.mov', 'chair_v001.mov', not really safe `my_chair_v001.mov` """ diff --git a/client/ayon_core/hosts/traypublisher/plugins/create/create_online.py b/client/ayon_core/hosts/traypublisher/plugins/create/create_online.py index a25da0bf34..f48037701e 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/create/create_online.py +++ b/client/ayon_core/hosts/traypublisher/plugins/create/create_online.py @@ -3,11 +3,12 @@ Online file retain their original name and use it as product name. To avoid conflicts, this creator checks if product with this name already -exists under selected asset. +exists under selected folder. """ from pathlib import Path -# from ayon_core.client import get_subset_by_name, get_asset_by_name +# import ayon_api + from ayon_core.lib.attribute_definitions import FileDef, BoolDef from ayon_core.pipeline import ( CreatedInstance, @@ -52,14 +53,14 @@ class OnlineCreator(TrayPublishCreator): # disable check for existing product with the same name """ - asset = get_asset_by_name( - self.project_name, instance_data["folderPath"], fields=["_id"]) + folder_entity = ayon_api.get_folder_by_path( + self.project_name, instance_data["folderPath"], fields={"id"}) - if get_subset_by_name( - self.project_name, origin_basename, asset["_id"], - fields=["_id"]): + if ayon_api.get_product_by_name( + self.project_name, origin_basename, folder_entity["id"], + fields={"id"}): raise CreatorError(f"product with {origin_basename} already " - "exists in selected asset") + "exists in selected folder") """ instance_data["originalBasename"] = origin_basename @@ -103,8 +104,8 @@ class OnlineCreator(TrayPublishCreator): def get_product_name( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=None, instance=None diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py index e8a2cae16c..4d203649c7 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_frame_data_from_asset_entity.py @@ -2,14 +2,14 @@ import pyblish.api class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): - """Collect Frame Data From AssetEntity found in context + """Collect Frame Data From 'folderEntity' found in context. Frame range data will only be collected if the keys are not yet collected for the instance. """ order = pyblish.api.CollectorOrder + 0.491 - label = "Collect Missing Frame Data From Asset" + label = "Collect Missing Frame Data From Folder" families = ["plate", "pointcache", "vdbcache", "online", "render"] @@ -27,11 +27,11 @@ class CollectFrameDataFromAssetEntity(pyblish.api.InstancePlugin): if key not in instance.data: missing_keys.append(key) keys_set = [] + folder_attributes = instance.data["folderEntity"]["attrib"] for key in missing_keys: - asset_data = instance.data["assetEntity"]["data"] - if key in asset_data: - instance.data[key] = asset_data[key] + if key in folder_attributes: + instance.data[key] = folder_attributes[key] keys_set.append(key) if keys_set: self.log.debug(f"Frame range data {keys_set} " - "has been collected from asset entity.") + "has been collected from folder entity.") diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_review_frames.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_review_frames.py index 6b41c0dd21..7eceda968a 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_review_frames.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_review_frames.py @@ -20,12 +20,12 @@ class CollectReviewInfo(pyblish.api.InstancePlugin): hosts = ["traypublisher"] def process(self, instance): - asset_entity = instance.data.get("assetEntity") - if instance.data.get("frameStart") is not None or not asset_entity: + folder_entity = instance.data.get("folderEntity") + if instance.data.get("frameStart") is not None or not folder_entity: self.log.debug("Missing required data on instance") return - asset_data = asset_entity["data"] + folder_attributes = folder_entity["attrib"] # Store collected data for logging collected_data = {} for key in ( @@ -35,9 +35,9 @@ class CollectReviewInfo(pyblish.api.InstancePlugin): "handleStart", "handleEnd", ): - if key in instance.data or key not in asset_data: + if key in instance.data or key not in folder_attributes: continue - value = asset_data[key] + value = folder_attributes[key] collected_data[key] = value instance.data[key] = value self.log.debug("Collected data: {}".format(str(collected_data))) diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py index 7eded0f6f5..bd5bac114d 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_sequence_frame_data.py @@ -28,9 +28,9 @@ class CollectSequenceFrameData( return # editorial would fail since they might not be in database yet - new_asset_publishing = instance.data.get("newAssetPublishing") - if new_asset_publishing: - self.log.debug("Instance is creating new asset. Skipping.") + new_folder_publishing = instance.data.get("newAssetPublishing") + if new_folder_publishing: + self.log.debug("Instance is creating new folders. Skipping.") return frame_data = self.get_frame_data_from_repre_sequence(instance) @@ -46,7 +46,7 @@ class CollectSequenceFrameData( def get_frame_data_from_repre_sequence(self, instance): repres = instance.data.get("representations") - asset_data = instance.data["assetEntity"]["data"] + folder_attributes = instance.data["folderEntity"]["attrib"] if repres: first_repre = repres[0] @@ -72,5 +72,5 @@ class CollectSequenceFrameData( "frameEnd": repres_frames[-1], "handleStart": 0, "handleEnd": 0, - "fps": asset_data["fps"] + "fps": folder_attributes["fps"] } diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py index d489528c57..edcbb27cb3 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/collect_shot_instances.py @@ -169,9 +169,8 @@ class CollectShotInstance(pyblish.api.InstancePlugin): parents = instance.data.get('parents', []) - # Split by '/' for AYON where asset is a path - asset_name = instance.data["folderPath"].split("/")[-1] - actual = {asset_name: in_info} + folder_name = instance.data["folderPath"].split("/")[-1] + actual = {folder_name: in_info} for parent in reversed(parents): parent_name = parent["entity_name"] diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml b/client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml index 726ccdffe3..89997b4c8c 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/help/validate_existing_version.xml @@ -5,7 +5,7 @@ ## Version already exists -Version {version} you have set on instance '{product_name}' under '{asset_name}' already exists. This validation is enabled by default to prevent accidental override of existing versions. +Version {version} you have set on instance '{product_name}' under '{folder_path}' already exists. This validation is enabled by default to prevent accidental override of existing versions. ### How to repair? - Click on 'Repair' action -> this will change version to next available. diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py index ddfe8904fa..3a62536507 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_existing_version.py @@ -40,7 +40,7 @@ class ValidateExistingVersion( formatting_data = { "product_name": product_name, - "asset_name": instance.data["folderPath"], + "folder_path": instance.data["folderPath"], "version": version } raise PublishXmlValidationError( diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py index cd4a98b84d..e5bf034d00 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_frame_ranges.py @@ -31,9 +31,9 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, return # editorial would fail since they might not be in database yet - new_asset_publishing = instance.data.get("newAssetPublishing") - if new_asset_publishing: - self.log.debug("Instance is creating new asset. Skipping.") + new_folder_publishing = instance.data.get("newAssetPublishing") + if new_folder_publishing: + self.log.debug("Instance is creating new folder. Skipping.") return if (self.skip_timelines_check and @@ -41,12 +41,11 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, for pattern in self.skip_timelines_check)): self.log.info("Skipping for {} task".format(instance.data["task"])) - asset_doc = instance.data["assetEntity"] - asset_data = asset_doc["data"] - frame_start = asset_data["frameStart"] - frame_end = asset_data["frameEnd"] - handle_start = asset_data["handleStart"] - handle_end = asset_data["handleEnd"] + folder_attributes = instance.data["folderEntity"]["attrib"] + frame_start = folder_attributes["frameStart"] + frame_end = folder_attributes["frameEnd"] + handle_start = folder_attributes["handleStart"] + handle_end = folder_attributes["handleEnd"] duration = (frame_end - frame_start + 1) + handle_start + handle_end repres = instance.data.get("representations") @@ -68,7 +67,7 @@ class ValidateFrameRange(OptionalPyblishPluginMixin, msg = ( "Frame duration from DB:'{}' doesn't match number of files:'{}'" - " Please change frame range for Asset or limit no. of files" + " Please change frame range for Folder or limit no. of files" ). format(int(duration), frames) formatting_data = {"duration": duration, diff --git a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_online_file.py b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_online_file.py index 3bd55342af..e9add2369b 100644 --- a/client/ayon_core/hosts/traypublisher/plugins/publish/validate_online_file.py +++ b/client/ayon_core/hosts/traypublisher/plugins/publish/validate_online_file.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +import ayon_api import pyblish.api from ayon_core.pipeline.publish import ( @@ -6,7 +7,6 @@ from ayon_core.pipeline.publish import ( PublishValidationError, OptionalPyblishPluginMixin, ) -from ayon_core.client import get_subset_by_name class ValidateOnlineFile(OptionalPyblishPluginMixin, @@ -23,12 +23,12 @@ class ValidateOnlineFile(OptionalPyblishPluginMixin, if not self.is_active(instance.data): return project_name = instance.context.data["projectName"] - asset_id = instance.data["assetEntity"]["_id"] - subset_doc = get_subset_by_name( - project_name, instance.data["productName"], asset_id) + folder_id = instance.data["folderEntity"]["id"] + product_entity = ayon_api.get_product_by_name( + project_name, instance.data["productName"], folder_id) - if subset_doc: + if product_entity: raise PublishValidationError( - "Subset to be published already exists.", + "Product to be published already exists.", title=self.label ) diff --git a/client/ayon_core/hosts/tvpaint/api/pipeline.py b/client/ayon_core/hosts/tvpaint/api/pipeline.py index 1b0227e89c..6f5c4d49d4 100644 --- a/client/ayon_core/hosts/tvpaint/api/pipeline.py +++ b/client/ayon_core/hosts/tvpaint/api/pipeline.py @@ -4,10 +4,9 @@ import tempfile import logging import requests - +import ayon_api import pyblish.api -from ayon_core.client import get_asset_by_name from ayon_core.host import HostBase, IWorkfileHost, ILoadHost, IPublishHost from ayon_core.hosts.tvpaint import TVPAINT_ROOT_DIR from ayon_core.settings import get_current_project_settings @@ -93,10 +92,10 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): return self.get_current_context().get("project_name") - def get_current_asset_name(self): + def get_current_folder_path(self): """ Returns: - Union[str, None]: Current asset name. + Union[str, None]: Current folder path. """ return self.get_current_context().get("folder_path") @@ -183,13 +182,13 @@ class TVPaintHost(HostBase, IWorkfileHost, ILoadHost, IPublishHost): log.info("Setting up project...") global_context = get_global_context() project_name = global_context.get("project_name") - asset_name = global_context.get("aset_name") - if not project_name or not asset_name: + folder_path = global_context.get("folder_path") + if not project_name or not folder_path: return - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) - set_context_settings(project_name, asset_doc) + set_context_settings(project_name, folder_entity) def application_exit(self): """Logic related to TimerManager. @@ -234,7 +233,7 @@ def containerise( "name": name, "namespace": namespace, "loader": str(loader), - "representation": str(context["representation"]["_id"]) + "representation": context["representation"]["id"] } if current_containers is None: current_containers = get_containers() @@ -466,17 +465,24 @@ def get_containers(): return output -def set_context_settings(project_name, asset_doc): - """Set workfile settings by asset document data. +def set_context_settings(project_name, folder_entity): + """Set workfile settings by folder entity attributes. Change fps, resolution and frame start/end. + + Args: + project_name (str): Project name. + folder_entity (dict[str, Any]): Folder entity. + """ - width_key = "resolutionWidth" - height_key = "resolutionHeight" + if not folder_entity: + return - width = asset_doc["data"].get(width_key) - height = asset_doc["data"].get(height_key) + folder_attributes = folder_entity["attrib"] + + width = folder_attributes.get("resolutionWidth") + height = folder_attributes.get("resolutionHeight") if width is None or height is None: print("Resolution was not found!") else: @@ -484,7 +490,7 @@ def set_context_settings(project_name, asset_doc): "tv_resizepage {} {} 0".format(width, height) ) - framerate = asset_doc["data"].get("fps") + framerate = folder_attributes.get("fps") if framerate is not None: execute_george( @@ -493,15 +499,15 @@ def set_context_settings(project_name, asset_doc): else: print("Framerate was not found!") - frame_start = asset_doc["data"].get("frameStart") - frame_end = asset_doc["data"].get("frameEnd") + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") if frame_start is None or frame_end is None: print("Frame range was not found!") return - handle_start = asset_doc["data"].get("handleStart") - handle_end = asset_doc["data"].get("handleEnd") + handle_start = folder_attributes.get("handleStart") + handle_end = folder_attributes.get("handleEnd") # Always start from 0 Mark In and set only Mark Out mark_in = 0 diff --git a/client/ayon_core/hosts/tvpaint/api/plugin.py b/client/ayon_core/hosts/tvpaint/api/plugin.py index ef9f82b783..e715b959f4 100644 --- a/client/ayon_core/hosts/tvpaint/api/plugin.py +++ b/client/ayon_core/hosts/tvpaint/api/plugin.py @@ -56,20 +56,29 @@ class TVPaintCreatorCommon: def _custom_get_product_name( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=None, instance=None ): dynamic_data = self.get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] return get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, self.product_type, variant, @@ -107,13 +116,15 @@ class TVPaintCreator(Creator, TVPaintCreatorCommon): self._remove_instance_from_context(instance) def get_dynamic_data(self, *args, **kwargs): - # Change asset and name by current workfile context + # Change folder and name by current workfile context create_context = self.create_context - asset_name = create_context.get_current_asset_name() + folder_path = create_context.get_current_folder_path() task_name = create_context.get_current_task_name() output = {} - if asset_name: - output["asset"] = asset_name + if folder_path: + folder_name = folder_path.rsplit("/")[-1] + output["asset"] = folder_name + output["folder"] = {"name": folder_name} if task_name: output["task"] = task_name return output @@ -152,22 +163,22 @@ class Loader(LoaderPlugin): ] return container["members"] - def get_unique_layer_name(self, asset_name, name): + def get_unique_layer_name(self, namespace, name): """Layer name with counter as suffix. Find higher 3 digit suffix from all layer names in scene matching regex - `{asset_name}_{name}_{suffix}`. Higher 3 digit suffix is used + `{namespace}_{name}_{suffix}`. Higher 3 digit suffix is used as base for next number if scene does not contain layer matching regex `0` is used ase base. Args: - asset_name (str): Name of product's parent asset document. + namespace (str): Usually folder name. name (str): Name of loaded product. Returns: - (str): `{asset_name}_{name}_{higher suffix + 1}` + str: `{namespace}_{name}_{higher suffix + 1}` """ - layer_name_base = "{}_{}".format(asset_name, name) + layer_name_base = "{}_{}".format(namespace, name) counter_regex = re.compile(r"_(\d{3})$") diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py b/client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py index 1415adac2b..34fe0ce8f4 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py +++ b/client/ayon_core/hosts/tvpaint/plugins/create/convert_legacy.py @@ -1,14 +1,14 @@ import collections from ayon_core.pipeline.create.creator_plugins import ( - SubsetConvertorPlugin, + ProductConvertorPlugin, cache_and_get_instances, ) from ayon_core.hosts.tvpaint.api.plugin import SHARED_DATA_KEY from ayon_core.hosts.tvpaint.api.lib import get_groups_data -class TVPaintLegacyConverted(SubsetConvertorPlugin): +class TVPaintLegacyConverted(ProductConvertorPlugin): """Conversion of legacy instances in scene to new creators. This convertor handles only instances created by core creators. diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py b/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py index dc53ccb9ca..8d91afc74e 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py +++ b/client/ayon_core/hosts/tvpaint/plugins/create/create_render.py @@ -37,7 +37,8 @@ Todos: import collections from typing import Any, Optional, Union -from ayon_core.client import get_asset_by_name, get_asset_name_identifier +import ayon_api + from ayon_core.lib import ( prepare_template_data, AbstractAttrDef, @@ -149,10 +150,21 @@ class CreateRenderlayer(TVPaintCreator): self.mark_for_review = plugin_settings["mark_for_review"] def get_dynamic_data( - self, project_name, asset_doc, task_name, variant, host_name, instance + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ): dynamic_data = super().get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) dynamic_data["renderpass"] = self.default_pass_name dynamic_data["renderlayer"] = variant @@ -208,7 +220,7 @@ class CreateRenderlayer(TVPaintCreator): creator_attributes["group_id"] = group_id creator_attributes["mark_for_review"] = mark_for_review - self.log.info(f"Subset name is {product_name}") + self.log.info(f"Product name is {product_name}") new_instance = CreatedInstance( self.product_type, product_name, @@ -425,10 +437,21 @@ class CreateRenderPass(TVPaintCreator): self._add_instance_to_context(instance) def get_dynamic_data( - self, project_name, asset_doc, task_name, variant, host_name, instance + self, + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ): dynamic_data = super().get_dynamic_data( - project_name, asset_doc, task_name, variant, host_name, instance + project_name, + folder_entity, + task_entity, + variant, + host_name, + instance ) dynamic_data["renderpass"] = variant dynamic_data["renderlayer"] = "{renderlayer}" @@ -754,8 +777,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def _prepare_render_layer( self, project_name: str, - asset_doc: dict[str, Any], - task_name: str, + folder_entity: dict[str, Any], + task_entity: dict[str, Any], group_id: int, groups: list[dict[str, Any]], mark_for_review: bool, @@ -772,6 +795,7 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): if not match_group: return None + task_name = task_entity["name"] variant: str = match_group["name"] creator: CreateRenderlayer = ( self.create_context.creators[CreateRenderlayer.identifier] @@ -779,20 +803,19 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): product_name: str = creator.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=self.create_context.host_name, ) - asset_name = get_asset_name_identifier(asset_doc) if existing_instance is not None: - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_entity["path"] existing_instance["task"] = task_name existing_instance["productName"] = product_name return existing_instance instance_data: dict[str, str] = { - "folderPath": asset_name, + "folderPath": folder_entity["path"], "task": task_name, "productType": creator.product_type, "variant": variant, @@ -806,13 +829,14 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def _prepare_render_passes( self, project_name: str, - asset_doc: dict[str, Any], - task_name: str, + folder_entity: dict[str, Any], + task_entity: dict[str, Any], render_layer_instance: CreatedInstance, layers: list[dict[str, Any]], mark_for_review: bool, existing_render_passes: list[CreatedInstance] ): + task_name = task_entity["name"] creator: CreateRenderPass = ( self.create_context.creators[CreateRenderPass.identifier] ) @@ -821,8 +845,6 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): for layer_name in render_pass["layer_names"]: render_pass_by_layer_name[layer_name] = render_pass - asset_name = get_asset_name_identifier(asset_doc) - for layer in layers: layer_name = layer["name"] variant = layer_name @@ -833,21 +855,21 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): product_name = creator.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=self.create_context.host_name, instance=render_pass ) if render_pass is not None: - render_pass["folderPath"] = asset_name + render_pass["folderPath"] = folder_entity["path"] render_pass["task"] = task_name render_pass["productName"] = product_name continue instance_data: dict[str, str] = { - "folderPath": asset_name, + "folderPath": folder_entity["path"], "task": task_name, "productType": creator.product_type, "variant": variant @@ -886,10 +908,13 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): def create(self, product_name, instance_data, pre_create_data): project_name: str = self.create_context.get_current_project_name() - asset_name: str = instance_data["folderPath"] + folder_path: str = instance_data["folderPath"] task_name: str = instance_data["task"] - asset_doc: dict[str, Any] = get_asset_by_name( - project_name, asset_name) + folder_entity: dict[str, Any] = ayon_api.get_folder_by_path( + project_name, folder_path) + task_entity: dict[str, Any] = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) render_layers_by_group_id: dict[int, CreatedInstance] = {} render_passes_by_render_layer_id: dict[int, list[CreatedInstance]] = ( @@ -951,8 +976,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): instance: Union[CreatedInstance, None] = ( self._prepare_render_layer( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, group_id, scene_groups, mark_layers_for_review, @@ -972,8 +997,8 @@ class TVPaintAutoDetectRenderCreator(TVPaintCreator): self._prepare_render_passes( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, render_layer_instance, layers, mark_passes_for_review, @@ -1047,16 +1072,16 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): def get_dynamic_data( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, instance ): dynamic_data = super().get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, instance @@ -1069,19 +1094,22 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): create_context = self.create_context host_name = create_context.host_name project_name = create_context.get_current_project_name() - asset_name = create_context.get_current_asset_name() + folder_path = create_context.get_current_folder_path() task_name = create_context.get_current_task_name() - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name, ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant, "creator_attributes": { @@ -1118,24 +1146,29 @@ class TVPaintSceneRenderCreator(TVPaintAutoCreator): create_context = self.create_context host_name = create_context.host_name project_name = create_context.get_current_project_name() - asset_name = create_context.get_current_asset_name() + folder_path = create_context.get_current_folder_path() task_name = create_context.get_current_task_name() existing_name = existing_instance.get("folderPath") if ( - existing_name != asset_name + existing_name != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, existing_instance["variant"], host_name, existing_instance ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_review.py b/client/ayon_core/hosts/tvpaint/plugins/create/create_review.py index 1837726cab..acb4f0f8d6 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_review.py +++ b/client/ayon_core/hosts/tvpaint/plugins/create/create_review.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_asset_by_name +import ayon_api + from ayon_core.pipeline import CreatedInstance from ayon_core.hosts.tvpaint.api.plugin import TVPaintAutoCreator @@ -30,25 +31,29 @@ class TVPaintReviewCreator(TVPaintAutoCreator): create_context = self.create_context host_name = create_context.host_name project_name = create_context.get_current_project_name() - asset_name = create_context.get_current_asset_name() + folder_path = create_context.get_current_folder_path() task_name = create_context.get_current_task_name() - if existing_instance is None: - existing_asset_name = None - else: - existing_asset_name = existing_instance["folderPath"] + existing_folder_path = None + if existing_instance is not None: + existing_folder_path = existing_instance["folderPath"] if existing_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant, } @@ -65,18 +70,23 @@ class TVPaintReviewCreator(TVPaintAutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_asset_name != asset_name + existing_folder_path != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, existing_instance["variant"], host_name, existing_instance ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py b/client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py index 14a11750a5..f21f41439e 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py +++ b/client/ayon_core/hosts/tvpaint/plugins/create/create_workfile.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_asset_by_name +import ayon_api + from ayon_core.pipeline import CreatedInstance from ayon_core.hosts.tvpaint.api.plugin import TVPaintAutoCreator @@ -26,25 +27,29 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): create_context = self.create_context host_name = create_context.host_name project_name = create_context.get_current_project_name() - asset_name = create_context.get_current_asset_name() + folder_path = create_context.get_current_folder_path() task_name = create_context.get_current_task_name() - if existing_instance is None: - existing_asset_name = None - else: - existing_asset_name = existing_instance["folderPath"] + existing_folder_path = None + if existing_instance is not None: + existing_folder_path = existing_instance["folderPath"] if existing_instance is None: - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, self.default_variant, host_name ) data = { - "folderPath": asset_name, + "folderPath": folder_path, "task": task_name, "variant": self.default_variant } @@ -58,18 +63,23 @@ class TVPaintWorkfileCreator(TVPaintAutoCreator): self._add_instance_to_context(new_instance) elif ( - existing_asset_name != asset_name + existing_folder_path != folder_path or existing_instance["task"] != task_name ): - asset_doc = get_asset_by_name(project_name, asset_name) + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) product_name = self.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, existing_instance["variant"], host_name, existing_instance ) - existing_instance["folderPath"] = asset_name + existing_instance["folderPath"] = folder_path existing_instance["task"] = task_name existing_instance["productName"] = product_name diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_image.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_image.py index 924c0f2835..21bbe56a54 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_image.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_image.py @@ -68,10 +68,10 @@ class ImportImage(plugin.Loader): load_options_str += (load_option + " ") # Prepare layer name - asset_name = context["asset"]["name"] + folder_name = context["folder"]["name"] version_name = context["version"]["name"] layer_name = "{}_{}_v{:0>3}".format( - asset_name, + folder_name, name, version_name ) diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py index 5e629f7b7f..2820b883ed 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_reference_image.py @@ -3,7 +3,7 @@ import collections from ayon_core.lib.attribute_definitions import BoolDef from ayon_core.pipeline import ( get_representation_context, - register_host, + registered_host, ) from ayon_core.hosts.tvpaint.api import plugin from ayon_core.hosts.tvpaint.api.lib import ( @@ -82,9 +82,9 @@ class LoadImage(plugin.Loader): load_options_str += (load_option + " ") # Prepare layer name - asset_name = context["asset"]["name"] - product_name = context["subset"]["name"] - layer_name = self.get_unique_layer_name(asset_name, product_name) + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + layer_name = self.get_unique_layer_name(folder_name, product_name) path = self.filepath_from_context(context) @@ -176,7 +176,7 @@ class LoadImage(plugin.Loader): return representation = container["representation"] members = self.get_members_from_container(container) - host = register_host() + host = registered_host() current_containers = host.get_containers() pop_idx = None for idx, cur_con in enumerate(current_containers): @@ -218,9 +218,9 @@ class LoadImage(plugin.Loader): removed. """ - repre_doc = context["representation"] + repre_entity = context["representation"] # Create new containers first - context = get_representation_context(repre_doc) + context = get_representation_context(repre_entity["id"]) # Get layer ids from previous container old_layer_names = self.get_members_from_container(container) diff --git a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py index 49ef9fc37b..4bb34089bd 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py +++ b/client/ayon_core/hosts/tvpaint/plugins/load/load_workfile.py @@ -51,26 +51,32 @@ class LoadWorkfile(plugin.Loader): # Save workfile. host_name = "tvpaint" - project_name = work_context.get("project") - asset_name = work_context.get("asset") - task_name = work_context.get("task") + if "project_name" in work_context: + project_name = context["project_name"] + folder_path = context["folder_path"] + task_name = context["task_name"] + else: + project_name = work_context.get("project") + folder_path = work_context.get("asset") + task_name = work_context.get("task") + # Far cases when there is workfile without work_context - if not asset_name: + if not folder_path: context = get_current_context() project_name = context["project_name"] - asset_name = context["folder_path"] + folder_path = context["folder_path"] task_name = context["task_name"] template_key = get_workfile_template_key_from_context( - asset_name, + project_name, + folder_path, task_name, host_name, - project_name=project_name ) anatomy = Anatomy(project_name) data = get_template_data_with_names( - project_name, asset_name, task_name, host_name + project_name, folder_path, task_name, host_name ) data["root"] = anatomy.roots diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py b/client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py index e7b7b2cad1..5f134a0cd0 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/collect_instance_frames.py @@ -15,14 +15,14 @@ class CollectOutputFrameRange(pyblish.api.InstancePlugin): families = ["review", "render"] def process(self, instance): - asset_doc = instance.data.get("assetEntity") - if not asset_doc: + folder_entity = instance.data.get("folderEntity") + if not folder_entity: return context = instance.context - frame_start = asset_doc["data"]["frameStart"] - fps = asset_doc["data"]["fps"] + frame_start = folder_entity["attrib"]["frameStart"] + fps = folder_entity["attrib"]["fps"] frame_end = frame_start + ( context.data["sceneMarkOut"] - context.data["sceneMarkIn"] ) diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py b/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py index 414b09c123..3155773bda 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/collect_workfile_data.py @@ -92,11 +92,11 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): os.environ[env_key] = workfile_context[key] self.log.info("Context changed to: {}".format(workfile_context)) - asset_name = workfile_context["folder_path"] + folder_path = workfile_context["folder_path"] task_name = workfile_context["task_name"] else: - asset_name = current_context["folder_path"] + folder_path = current_context["folder_path"] task_name = current_context["task_name"] # Handle older workfiles or workfiles without metadata self.log.warning(( @@ -104,12 +104,12 @@ class CollectWorkfileData(pyblish.api.ContextPlugin): " Using current Session context." )) - # Store context asset name - context.data["folderPath"] = asset_name + # Store context folder path + context.data["folderPath"] = folder_path context.data["task"] = task_name self.log.info( - "Context is set to Asset: \"{}\" and Task: \"{}\"".format( - asset_name, task_name + "Context is set to Folder: \"{}\" and Task: \"{}\"".format( + folder_path, task_name ) ) diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml b/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml index 83753b3410..bba0104c54 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/help/validate_asset_name.xml @@ -1,14 +1,14 @@ -Subset context +Product context ## Invalid product context Context of the given product doesn't match your current scene. ### How to repair? -Yout can fix this with "Repair" button on the right. This will use '{expected_asset}' asset name and overwrite '{found_asset}' asset name in scene metadata. +Yout can fix this with "Repair" button on the right. This will use '{expected_folder}' folder path and overwrite '{found_folder}' folder path in scene metadata. After that restart publishing with Reload button. diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py b/client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py index 927d601e34..764c090720 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/validate_asset_name.py @@ -9,8 +9,8 @@ from ayon_core.hosts.tvpaint.api.pipeline import ( ) -class FixAssetNames(pyblish.api.Action): - """Repair the asset names. +class FixFolderPaths(pyblish.api.Action): + """Repair the folder paths. Change instanace metadata in the workfile. """ @@ -20,16 +20,16 @@ class FixAssetNames(pyblish.api.Action): on = "failed" def process(self, context, plugin): - context_asset_name = context.data["folderPath"] + context_folder_path = context.data["folderPath"] old_instance_items = list_instances() new_instance_items = [] for instance_item in old_instance_items: - instance_asset_name = instance_item.get("folderPath") + instance_folder_path = instance_item.get("folderPath") if ( - instance_asset_name - and instance_asset_name != context_asset_name + instance_folder_path + and instance_folder_path != context_folder_path ): - instance_item["folderPath"] = context_asset_name + instance_item["folderPath"] = context_folder_path new_instance_items.append(instance_item) write_instances(new_instance_items) @@ -38,23 +38,23 @@ class ValidateAssetName( OptionalPyblishPluginMixin, pyblish.api.ContextPlugin ): - """Validate asset name present on instance. + """Validate folder path present on instance. - Asset name on instance should be the same as context's. + Folder path on instance should be the same as context's. """ - label = "Validate Asset Names" + label = "Validate Folder Paths" order = pyblish.api.ValidatorOrder hosts = ["tvpaint"] - actions = [FixAssetNames] + actions = [FixFolderPaths] def process(self, context): if not self.is_active(context.data): return - context_asset_name = context.data["folderPath"] + context_folder_path = context.data["folderPath"] for instance in context: - asset_name = instance.data.get("folderPath") - if asset_name and asset_name == context_asset_name: + folder_path = instance.data.get("folderPath") + if folder_path and folder_path == context_folder_path: continue instance_label = ( @@ -64,14 +64,14 @@ class ValidateAssetName( raise PublishXmlValidationError( self, ( - "Different asset name on instance then context's." - " Instance \"{}\" has asset name: \"{}\"" - " Context asset name is: \"{}\"" + "Different folder path on instance then context's." + " Instance \"{}\" has folder path: \"{}\"" + " Context folder path is: \"{}\"" ).format( - instance_label, asset_name, context_asset_name + instance_label, folder_path, context_folder_path ), formatting_data={ - "expected_asset": context_asset_name, - "found_asset": asset_name + "expected_folder": context_folder_path, + "found_folder": folder_path } ) diff --git a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py b/client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py index 2268e59d88..5e42b5ab2f 100644 --- a/client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py +++ b/client/ayon_core/hosts/tvpaint/plugins/publish/validate_scene_settings.py @@ -22,7 +22,7 @@ class ValidateProjectSettings( if not self.is_active(context.data): return - expected_data = context.data["assetEntity"]["data"] + folder_attributes = context.data["folderEntity"]["attrib"] scene_data = { "fps": context.data.get("sceneFps"), "resolutionWidth": context.data.get("sceneWidth"), @@ -31,7 +31,7 @@ class ValidateProjectSettings( } invalid = {} for k in scene_data.keys(): - expected_value = expected_data[k] + expected_value = folder_attributes[k] if scene_data[k] != expected_value: invalid[k] = { "current": scene_data[k], "expected": expected_value @@ -46,13 +46,13 @@ class ValidateProjectSettings( json.dumps(invalid, sort_keys=True, indent=4) ), formatting_data={ - "expected_fps": expected_data["fps"], + "expected_fps": folder_attributes["fps"], "current_fps": scene_data["fps"], - "expected_width": expected_data["resolutionWidth"], - "expected_height": expected_data["resolutionHeight"], + "expected_width": folder_attributes["resolutionWidth"], + "expected_height": folder_attributes["resolutionHeight"], "current_width": scene_data["resolutionWidth"], "current_height": scene_data["resolutionHeight"], - "expected_pixel_ratio": expected_data["pixelAspect"], + "expected_pixel_ratio": folder_attributes["pixelAspect"], "current_pixel_ratio": scene_data["pixelAspect"] } ) diff --git a/client/ayon_core/hosts/unreal/api/pipeline.py b/client/ayon_core/hosts/unreal/api/pipeline.py index 922fc8abd8..1f937437a4 100644 --- a/client/ayon_core/hosts/unreal/api/pipeline.py +++ b/client/ayon_core/hosts/unreal/api/pipeline.py @@ -4,12 +4,12 @@ import json import logging from typing import List from contextlib import contextmanager -import semver import time +import semver import pyblish.api +import ayon_api -from ayon_core.client import get_asset_by_name, get_assets from ayon_core.pipeline import ( register_loader_plugin_path, register_creator_plugin_path, @@ -262,7 +262,7 @@ def containerise(name, namespace, nodes, context, loader=None, suffix="_CON"): "name": new_name, "namespace": namespace, "loader": str(loader), - "representation": context["representation"]["_id"], + "representation": context["representation"]["id"], } # 3 - imprint data imprint(f"{path}/{container_name}", data) @@ -601,34 +601,36 @@ def generate_sequence(h, h_dir): ) project_name = get_current_project_name() - asset_data = get_asset_by_name( + # TODO Fix this does not return folder path + folder_path = h_dir.split('/')[-1], + folder_entity = ayon_api.get_folder_by_path( project_name, - h_dir.split('/')[-1], - fields=["_id", "data.fps"] + folder_path, + fields={"id", "attrib.fps"} ) start_frames = [] end_frames = [] - elements = list(get_assets( + elements = list(ayon_api.get_folders( project_name, - parent_ids=[asset_data["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] + parent_ids=[folder_entity["id"]], + fields={"id", "attrib.clipIn", "attrib.clipOut"} )) for e in elements: - start_frames.append(e.get('data').get('clipIn')) - end_frames.append(e.get('data').get('clipOut')) + start_frames.append(e["attrib"].get("clipIn")) + end_frames.append(e["attrib"].get("clipOut")) - elements.extend(get_assets( + elements.extend(ayon_api.get_folders( project_name, - parent_ids=[e["_id"]], - fields=["_id", "data.clipIn", "data.clipOut"] + parent_ids=[e["id"]], + fields={"id", "attrib.clipIn", "attrib.clipOut"} )) min_frame = min(start_frames) max_frame = max(end_frames) - fps = asset_data.get('data').get("fps") + fps = folder_entity["attrib"].get("fps") sequence.set_display_rate( unreal.FrameRate(fps, 1.0)) diff --git a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py index 0eaa1adb84..2f78bf026d 100644 --- a/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py +++ b/client/ayon_core/hosts/unreal/hooks/pre_workfile_preparation.py @@ -49,7 +49,7 @@ class UnrealPrelaunchHook(PreLaunchHook): # Prepare data for fill data and for getting workfile template key anatomy = self.data["anatomy"] - project_doc = self.data["project_doc"] + project_entity = self.data["project_entity"] # Use already prepared workdir data workdir_data = copy.deepcopy(self.data["workdir_data"]) @@ -61,9 +61,9 @@ class UnrealPrelaunchHook(PreLaunchHook): # Get workfile template key for current context workfile_template_key = get_workfile_template_key( + project_entity["name"], task_type, self.host_name, - project_name=project_doc["name"] ) # Fill templates template_obj = anatomy.templates_obj[workfile_template_key]["file"] diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py index a4fcda6ade..9b8d22fb5e 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_alembic_animation.py @@ -70,22 +70,24 @@ class AnimationAlembicLoader(plugin.Loader): # Create directory for asset and ayon container root = unreal_pipeline.AYON_ASSET_DIR - asset = context.get('asset').get('name') + folder_name = context["folder"]["name"] + folder_path = context["folder"]["path"] + product_type = context["representation"]["context"]["family"] suffix = "_CON" - if asset: - asset_name = "{}_{}".format(asset, name) + if folder_name: + asset_name = "{}_{}".format(folder_name, name) else: asset_name = "{}".format(name) - version = context.get('version') + version = context["version"]["version"] # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": + if version < 0: name_version = f"{name}_hero" else: - name_version = f"{name}_v{version.get('name'):03d}" + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name_version}", suffix="") + f"{root}/{folder_name}/{name_version}", suffix="") container_name += suffix @@ -105,14 +107,17 @@ class AnimationAlembicLoader(plugin.Loader): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], + "product_type": product_type, + # TODO these should be probably removed + "asset": folder_path, + "family": product_type, } unreal_pipeline.imprint( f"{asset_dir}/{container_name}", data) @@ -128,8 +133,8 @@ class AnimationAlembicLoader(plugin.Loader): def update(self, container, context): folder_name = container["asset_name"] - repre_doc = context["representation"] - source_path = get_representation_path(repre_doc) + repre_entity = context["representation"] + source_path = get_representation_path(repre_entity) destination_path = container["namespace"] task = self.get_task( @@ -146,8 +151,8 @@ class AnimationAlembicLoader(plugin.Loader): unreal_pipeline.imprint( container_path, { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]) + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], }) asset_content = unreal.EditorAssetLibrary.list_assets( diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_animation.py b/client/ayon_core/hosts/unreal/plugins/load/load_animation.py index 3aad6886be..b2c066c871 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_animation.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_animation.py @@ -8,7 +8,7 @@ from unreal import EditorAssetLibrary from unreal import MovieSceneSkeletalAnimationTrack from unreal import MovieSceneSkeletalAnimationSection -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.pipeline import ( get_representation_path, AYON_CONTAINER_ID @@ -53,7 +53,7 @@ class AnimationFBXLoader(plugin.Loader): if not actor: return None - asset_doc = get_current_project_asset(fields=["data.fps"]) + folder_entity = get_current_project_folder(fields=["attrib.fps"]) task.set_editor_property('filename', path) task.set_editor_property('destination_path', asset_dir) @@ -82,7 +82,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) + 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -140,14 +140,17 @@ class AnimationFBXLoader(plugin.Loader): list(str): list of container content """ # Create directory for asset and Ayon container - hierarchy = context.get('asset').get('data').get('parents') root = "/Game/Ayon" - asset = context.get('asset').get('name') + folder_path = context["folder"]["path"] + hierarchy = folder_path.lstrip("/").split("/") + folder_name = hierarchy.pop(-1) + product_type = context["product"]["productType"] + suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/Animations/{asset}/{name}", suffix="") + f"{root}/Animations/{folder_name}/{name}", suffix="") ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -161,7 +164,7 @@ class AnimationFBXLoader(plugin.Loader): hierarchy_dir = root for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" - hierarchy_dir = f"{hierarchy_dir}/{asset}" + hierarchy_dir = f"{hierarchy_dir}/{folder_name}" _filter = unreal.ARFilter( class_names=["World"], @@ -226,14 +229,17 @@ class AnimationFBXLoader(plugin.Loader): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], + "folder_path": folder_path, + "product_type": product_type, + # TODO these shold be probably removed + "asset": folder_path, + "family": product_type } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -247,10 +253,10 @@ class AnimationFBXLoader(plugin.Loader): unreal.EditorLevelLibrary.load_level(master_level) def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] folder_name = container["asset_name"] - source_path = get_representation_path(repre_doc) - asset_doc = get_current_project_asset(fields=["data.fps"]) + source_path = get_representation_path(repre_entity) + folder_entity = get_current_project_folder(fields=["attrib.fps"]) destination_path = container["namespace"] task = unreal.AssetImportTask() @@ -284,7 +290,7 @@ class AnimationFBXLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) + 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -306,8 +312,8 @@ class AnimationFBXLoader(plugin.Loader): unreal_pipeline.imprint( container_path, { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]) + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], }) asset_content = EditorAssetLibrary.list_assets( diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_camera.py b/client/ayon_core/hosts/unreal/plugins/load/load_camera.py index 34c1e3e023..ed159d31bd 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_camera.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_camera.py @@ -2,6 +2,8 @@ """Load camera from FBX.""" from pathlib import Path +import ayon_api + import unreal from unreal import ( EditorAssetLibrary, @@ -9,10 +11,10 @@ from unreal import ( EditorLevelUtils, LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, ) -from ayon_core.client import get_asset_by_name from ayon_core.pipeline import ( AYON_CONTAINER_ID, get_current_project_name, + get_representation_path, ) from ayon_core.hosts.unreal.api import plugin from ayon_core.hosts.unreal.api.pipeline import ( @@ -83,24 +85,33 @@ class CameraLoader(plugin.Loader): """ # Create directory for asset and Ayon container - hierarchy = context.get('asset').get('data').get('parents') + folder_entity = context["folder"] + folder_attributes = folder_entity["attrib"] + folder_path = folder_entity["path"] + hierarchy_parts = folder_path.split("/") + # Remove empty string + hierarchy_parts.pop(0) + # Pop folder name + folder_name = hierarchy_parts.pop(-1) + root = "/Game/Ayon" hierarchy_dir = root hierarchy_dir_list = [] - for h in hierarchy: + for h in hierarchy_parts: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir_list.append(hierarchy_dir) - asset = context.get('asset').get('name') suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" + asset_name = f"{folder_name}_{name}" if folder_name else name tools = unreal.AssetToolsHelpers().get_asset_tools() # Create a unique name for the camera directory unique_number = 1 - if EditorAssetLibrary.does_directory_exist(f"{hierarchy_dir}/{asset}"): + if EditorAssetLibrary.does_directory_exist( + f"{hierarchy_dir}/{folder_name}" + ): asset_content = EditorAssetLibrary.list_assets( - f"{root}/{asset}", recursive=False, include_folder=True + f"{root}/{folder_name}", recursive=False, include_folder=True ) # Get highest number to make a unique name @@ -113,7 +124,7 @@ class CameraLoader(plugin.Loader): unique_number = f_numbers[-1] + 1 if f_numbers else 1 asset_dir, container_name = tools.create_unique_asset_name( - f"{hierarchy_dir}/{asset}/{name}_{unique_number:02d}", suffix="") + f"{hierarchy_dir}/{folder_name}/{name}_{unique_number:02d}", suffix="") container_name += suffix @@ -122,14 +133,18 @@ class CameraLoader(plugin.Loader): # Create map for the shot, and create hierarchy of map. If the maps # already exist, we will use them. h_dir = hierarchy_dir_list[0] - h_asset = hierarchy[0] + h_asset = hierarchy_dir[0] master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" if not EditorAssetLibrary.does_asset_exist(master_level): EditorLevelLibrary.new_level(f"{h_dir}/{h_asset}_map") - level = f"{asset_dir}/{asset}_map_camera.{asset}_map_camera" + level = ( + f"{asset_dir}/{folder_name}_map_camera.{folder_name}_map_camera" + ) if not EditorAssetLibrary.does_asset_exist(level): - EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map_camera") + EditorLevelLibrary.new_level( + f"{asset_dir}/{folder_name}_map_camera" + ) EditorLevelLibrary.load_level(master_level) EditorLevelUtils.add_level_to_world( @@ -144,7 +159,7 @@ class CameraLoader(plugin.Loader): # they don't exist. frame_ranges = [] sequences = [] - for (h_dir, h) in zip(hierarchy_dir_list, hierarchy): + for (h_dir, h) in zip(hierarchy_dir_list, hierarchy_parts): root_content = EditorAssetLibrary.list_assets( h_dir, recursive=False, include_folder=False) @@ -170,7 +185,7 @@ class CameraLoader(plugin.Loader): EditorAssetLibrary.make_directory(asset_dir) cam_seq = tools.create_asset( - asset_name=f"{asset}_camera", + asset_name=f"{folder_name}_camera", package_path=asset_dir, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew() @@ -184,16 +199,17 @@ class CameraLoader(plugin.Loader): frame_ranges[i + 1][0], frame_ranges[i + 1][1], [level]) - project_name = get_current_project_name() - data = get_asset_by_name(project_name, asset)["data"] + clip_in = folder_attributes.get("clipIn") + clip_out = folder_attributes.get("clipOut") + cam_seq.set_display_rate( - unreal.FrameRate(data.get("fps"), 1.0)) - cam_seq.set_playback_start(data.get('clipIn')) - cam_seq.set_playback_end(data.get('clipOut') + 1) + unreal.FrameRate(folder_attributes.get("fps"), 1.0)) + cam_seq.set_playback_start(clip_in) + cam_seq.set_playback_end(clip_out + 1) set_sequence_hierarchy( sequences[-1], cam_seq, frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), + clip_in, clip_out, [level]) settings = unreal.MovieSceneUserImportFBXSettings() @@ -215,9 +231,7 @@ class CameraLoader(plugin.Loader): for possessable in cam_seq.get_possessables(): for tracks in possessable.get_tracks(): for section in tracks.get_sections(): - section.set_range( - data.get('clipIn'), - data.get('clipOut') + 1) + section.set_range(clip_in, clip_out + 1) for channel in section.get_all_channels(): for key in channel.get_keys(): old_time = key.get_time().get_editor_property( @@ -225,7 +239,7 @@ class CameraLoader(plugin.Loader): old_time_value = old_time.get_editor_property( 'value') new_time = old_time_value + ( - data.get('clipIn') - data.get('frameStart') + clip_in - folder_attributes.get('frameStart') ) key.set_time(unreal.FrameNumber(value=new_time)) @@ -233,17 +247,21 @@ class CameraLoader(plugin.Loader): create_container( container=container_name, path=asset_dir) + product_type = context["product"]["productType"] data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], + "product_type": product_type, + # TODO these should be probably removed + "asset": folder_name, + "family": product_type, } imprint(f"{asset_dir}/{container_name}", data) @@ -379,28 +397,33 @@ class CameraLoader(plugin.Loader): sub_scene.set_sequence(new_sequence) - repre_doc = context["representation"] + repre_entity = context["representation"] + repre_path = get_representation_path(repre_entity) self._import_camera( EditorLevelLibrary.get_editor_world(), new_sequence, new_sequence.get_bindings(), settings, - str(repre_doc["data"]["path"]) + repre_path ) # Set range of all sections # Changing the range of the section is not enough. We need to change # the frame of all the keys in the section. project_name = get_current_project_name() - asset = container.get('asset') - data = get_asset_by_name(project_name, asset)["data"] + folder_path = container.get("folder_path") + if folder_path is None: + folder_path = container.get("asset") + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + folder_attributes = folder_entity["attrib"] + clip_in = folder_attributes["clipIn"] + clip_out = folder_attributes["clipOut"] + frame_start = folder_attributes["frameStart"] for possessable in new_sequence.get_possessables(): for tracks in possessable.get_tracks(): for section in tracks.get_sections(): - section.set_range( - data.get('clipIn'), - data.get('clipOut') + 1) + section.set_range(clip_in, clip_out + 1) for channel in section.get_all_channels(): for key in channel.get_keys(): old_time = key.get_time().get_editor_property( @@ -408,13 +431,13 @@ class CameraLoader(plugin.Loader): old_time_value = old_time.get_editor_property( 'value') new_time = old_time_value + ( - data.get('clipIn') - data.get('frameStart') + clip_in - frame_start ) key.set_time(unreal.FrameNumber(value=new_time)) data = { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]) + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], } imprint(f"{asset_dir}/{container.get('container_name')}", data) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py index bca99f202f..d0c936ed06 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_geometrycache_abc.py @@ -84,22 +84,32 @@ class PointCacheAlembicLoader(plugin.Loader): create_container(container=container_name, path=asset_dir) def imprint( - self, asset, asset_dir, container_name, asset_name, representation, - frame_start, frame_end + self, + folder_path, + asset_dir, + container_name, + asset_name, + representation, + frame_start, + frame_end, + product_type, ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": representation["_id"], - "parent": representation["parent"], - "family": representation["context"]["family"], + "representation": representation["id"], + "parent": representation["versionId"], "frame_start": frame_start, - "frame_end": frame_end + "frame_end": frame_end, + "product_type": product_type, + "folder_path": folder_path, + # TODO these should be probably removed + "family": product_type, + "asset": folder_path, } imprint(f"{asset_dir}/{container_name}", data) @@ -119,24 +129,28 @@ class PointCacheAlembicLoader(plugin.Loader): list(str): list of container content """ # Create directory for asset and Ayon container - asset = context.get('asset').get('name') + folder_entity = context["folder"] + folder_path = folder_entity["path"] + folder_name = folder_entity["name"] + folder_attributes = folder_entity["attrib"] + suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version') + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" + version = context["version"]["version"] # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": + if version < 0: name_version = f"{name}_hero" else: - name_version = f"{name}_v{version.get('name'):03d}" + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{asset}/{name_version}", suffix="") + f"{self.root}/{folder_name}/{name_version}", suffix="") container_name += suffix - frame_start = context.get('asset').get('data').get('frameStart') - frame_end = context.get('asset').get('data').get('frameEnd') + frame_start = folder_attributes.get("frameStart") + frame_end = folder_attributes.get("frameEnd") # If frame start and end are the same, we increase the end frame by # one, otherwise Unreal will not import it @@ -151,8 +165,15 @@ class PointCacheAlembicLoader(plugin.Loader): frame_start, frame_end) self.imprint( - asset, asset_dir, container_name, asset_name, - context["representation"], frame_start, frame_end) + folder_path, + asset_dir, + container_name, + asset_name, + context["representation"], + frame_start, + frame_end, + context["product"]["productType"] + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -164,14 +185,13 @@ class PointCacheAlembicLoader(plugin.Loader): return asset_content def update(self, container, context): - asset_doc = context["asset"] - subset_doc = context["subset"] - version_doc = context["version"] - repre_doc = context["representation"] - - # Create directory for asset and Ayon container - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + # Create directory for folder and Ayon container + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + product_type = context["product"]["productType"] + version = context["version"]["version"] + repre_entity = context["representation"] suffix = "_CON" asset_name = product_name @@ -179,7 +199,6 @@ class PointCacheAlembicLoader(plugin.Loader): asset_name = f"{folder_name}_{product_name}" # Check if version is hero version and use different name - version = version_doc.get("name", -1) if version < 0: name_version = f"{product_name}_hero" else: @@ -194,15 +213,22 @@ class PointCacheAlembicLoader(plugin.Loader): frame_end = int(container.get("frame_end")) if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) self.import_and_containerize( path, asset_dir, asset_name, container_name, frame_start, frame_end) self.imprint( - folder_name, asset_dir, container_name, asset_name, repre_doc, - frame_start, frame_end) + folder_path, + asset_dir, + container_name, + asset_name, + repre_entity, + frame_start, + frame_end, + product_type + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout.py index f78dba9e57..beb94ada7b 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_layout.py @@ -15,8 +15,8 @@ from unreal import ( MovieSceneSubTrack, LevelSequenceEditorBlueprintLibrary as LevelSequenceLib, ) +import ayon_api -from ayon_core.client import get_asset_by_name, get_representations from ayon_core.pipeline import ( discover_loader_plugins, loaders_from_representation, @@ -25,7 +25,7 @@ from ayon_core.pipeline import ( AYON_CONTAINER_ID, get_current_project_name, ) -from ayon_core.pipeline.context_tools import get_current_project_asset +from ayon_core.pipeline.context_tools import get_current_project_folder from ayon_core.settings import get_current_project_settings from ayon_core.hosts.unreal.api import plugin from ayon_core.hosts.unreal.api.pipeline import ( @@ -169,7 +169,7 @@ class LayoutLoader(plugin.Loader): anim_path = f"{asset_dir}/animations/{anim_file_name}" - asset_doc = get_current_project_asset() + folder_entity = get_current_project_folder() # Import animation task = unreal.AssetImportTask() task.options = unreal.FbxImportUI() @@ -204,7 +204,7 @@ class LayoutLoader(plugin.Loader): task.options.anim_sequence_import_data.set_editor_property( 'use_default_sample_rate', False) task.options.anim_sequence_import_data.set_editor_property( - 'custom_sample_rate', asset_doc.get("data", {}).get("fps")) + 'custom_sample_rate', folder_entity.get("attrib", {}).get("fps")) task.options.anim_sequence_import_data.set_editor_property( 'import_custom_attribute', True) task.options.anim_sequence_import_data.set_editor_property( @@ -290,7 +290,7 @@ class LayoutLoader(plugin.Loader): sec_params = section.get_editor_property('params') sec_params.set_editor_property('animation', animation) - def _get_repre_docs_by_version_id(self, data): + def _get_repre_entities_by_version_id(self, data): version_ids = { element.get("version") for element in data @@ -303,15 +303,15 @@ class LayoutLoader(plugin.Loader): return output project_name = get_current_project_name() - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, - representation_names=["fbx", "abc"], + representation_names={"fbx", "abc"}, version_ids=version_ids, - fields=["_id", "parent", "name"] + fields={"id", "versionId", "name"} ) - for repre_doc in repre_docs: - version_id = str(repre_doc["parent"]) - output[version_id].append(repre_doc) + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + output[version_id].append(repre_entity) return output def _process(self, lib_path, asset_dir, sequence, repr_loaded=None): @@ -333,47 +333,50 @@ class LayoutLoader(plugin.Loader): loaded_assets = [] - repre_docs_by_version_id = self._get_repre_docs_by_version_id(data) + repre_entities_by_version_id = self._get_repre_entities_by_version_id( + data + ) for element in data: - representation = None + repre_id = None repr_format = None if element.get('representation'): - repre_docs = repre_docs_by_version_id[element.get("version")] - if not repre_docs: + version_id = element.get("version") + repre_entities = repre_entities_by_version_id[version_id] + if not repre_entities: self.log.error( - f"No valid representation found for version " - f"{element.get('version')}") + f"No valid representation found for version" + f" {version_id}") continue - repre_doc = repre_docs[0] - representation = str(repre_doc["_id"]) - repr_format = repre_doc["name"] + repre_entity = repre_entities[0] + repre_id = repre_entity["id"] + repr_format = repre_entity["name"] # This is to keep compatibility with old versions of the # json format. elif element.get('reference_fbx'): - representation = element.get('reference_fbx') + repre_id = element.get('reference_fbx') repr_format = 'fbx' elif element.get('reference_abc'): - representation = element.get('reference_abc') + repre_id = element.get('reference_abc') repr_format = 'abc' # If reference is None, this element is skipped, as it cannot be # imported in Unreal - if not representation: + if not repre_id: continue instance_name = element.get('instance_name') skeleton = None - if representation not in repr_loaded: - repr_loaded.append(representation) + if repre_id not in repr_loaded: + repr_loaded.append(repre_id) product_type = element.get("product_type") if product_type is None: product_type = element.get("family") loaders = loaders_from_representation( - all_loaders, representation) + all_loaders, repre_id) loader = None @@ -384,7 +387,7 @@ class LayoutLoader(plugin.Loader): if not loader: self.log.error( - f"No valid loader found for {representation}") + f"No valid loader found for {repre_id}") continue options = { @@ -393,7 +396,7 @@ class LayoutLoader(plugin.Loader): assets = load_container( loader, - representation, + repre_id, namespace=instance_name, options=options ) @@ -413,8 +416,8 @@ class LayoutLoader(plugin.Loader): item for item in data if ((item.get('version') and item.get('version') == element.get('version')) or - item.get('reference_fbx') == representation or - item.get('reference_abc') == representation)] + item.get('reference_fbx') == repre_id or + item.get('reference_abc') == repre_id)] for instance in instances: # transform = instance.get('transform') @@ -438,9 +441,9 @@ class LayoutLoader(plugin.Loader): bindings_dict[inst] = bindings if skeleton: - skeleton_dict[representation] = skeleton + skeleton_dict[repre_id] = skeleton else: - skeleton = skeleton_dict.get(representation) + skeleton = skeleton_dict.get(repre_id) animation_file = element.get('animation') @@ -518,20 +521,25 @@ class LayoutLoader(plugin.Loader): create_sequences = data["unreal"]["level_sequences_for_layouts"] # Create directory for asset and Ayon container - hierarchy = context.get('asset').get('data').get('parents') + folder_entity = context["folder"] + folder_path = folder_entity["path"] + hierarchy = folder_path.lstrip("/").split("/") + # Remove folder name + folder_name = hierarchy.pop(-1) root = self.ASSET_ROOT hierarchy_dir = root hierarchy_dir_list = [] for h in hierarchy: hierarchy_dir = f"{hierarchy_dir}/{h}" hierarchy_dir_list.append(hierarchy_dir) - asset = context.get('asset').get('name') suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else name + asset_name = f"{folder_name}_{name}" if folder_name else name tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - "{}/{}/{}".format(hierarchy_dir, asset, name), suffix="") + "{}/{}/{}".format(hierarchy_dir, folder_name, name), + suffix="" + ) container_name += suffix @@ -541,8 +549,8 @@ class LayoutLoader(plugin.Loader): shot = None sequences = [] - level = f"{asset_dir}/{asset}_map.{asset}_map" - EditorLevelLibrary.new_level(f"{asset_dir}/{asset}_map") + level = f"{asset_dir}/{folder_name}_map.{folder_name}_map" + EditorLevelLibrary.new_level(f"{asset_dir}/{folder_name}_map") if create_sequences: # Create map for the shot, and create hierarchy of map. If the @@ -591,7 +599,7 @@ class LayoutLoader(plugin.Loader): e.get_asset().get_playback_end())) shot = tools.create_asset( - asset_name=asset, + asset_name=folder_name, package_path=asset_dir, asset_class=unreal.LevelSequence, factory=unreal.LevelSequenceFactoryNew() @@ -606,16 +614,24 @@ class LayoutLoader(plugin.Loader): [level]) project_name = get_current_project_name() - data = get_asset_by_name(project_name, asset)["data"] + folder_attributes = ( + ayon_api.get_folder_by_path(project_name, folder_path)["attrib"] + ) shot.set_display_rate( - unreal.FrameRate(data.get("fps"), 1.0)) + unreal.FrameRate(folder_attributes.get("fps"), 1.0)) shot.set_playback_start(0) - shot.set_playback_end(data.get('clipOut') - data.get('clipIn') + 1) + shot.set_playback_end( + folder_attributes.get('clipOut') + - folder_attributes.get('clipIn') + + 1 + ) if sequences: set_sequence_hierarchy( - sequences[-1], shot, + sequences[-1], + shot, frame_ranges[-1][1], - data.get('clipIn'), data.get('clipOut'), + folder_attributes.get('clipIn'), + folder_attributes.get('clipOut'), [level]) EditorLevelLibrary.load_level(level) @@ -635,13 +651,14 @@ class LayoutLoader(plugin.Loader): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "asset": folder_name, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], "family": context["representation"]["context"]["family"], "loaded_assets": loaded_assets } @@ -678,17 +695,18 @@ class LayoutLoader(plugin.Loader): asset_dir = container.get('namespace') - asset_doc = context["asset"] - repre_doc = context["representation"] + folder_entity = context["folder"] + repre_entity = context["representation"] - hierarchy = list(asset_doc["data"]["parents"]) + hierarchy = folder_entity["path"].lstrip("/").split("/") + first_parent_name = hierarchy[0] sequence = None master_level = None if create_sequences: - h_dir = f"{root}/{hierarchy[0]}" - h_asset = hierarchy[0] + h_dir = f"{root}/{first_parent_name}" + h_asset = first_parent_name master_level = f"{h_dir}/{h_asset}_map.{h_asset}_map" filter = unreal.ARFilter( @@ -730,21 +748,21 @@ class LayoutLoader(plugin.Loader): EditorAssetLibrary.delete_directory(f"{asset_dir}/animations/") - source_path = get_representation_path(repre_doc) + source_path = get_representation_path(repre_entity) loaded_assets = self._process(source_path, asset_dir, sequence) data = { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]), - "loaded_assets": loaded_assets + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], + "loaded_assets": loaded_assets, } imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) EditorLevelLibrary.save_current_level() - save_dir = f"{root}/{hierarchy[0]}" if create_sequences else asset_dir + save_dir = f"{root}/{first_parent_name}" if create_sequences else asset_dir asset_content = EditorAssetLibrary.list_assets( save_dir, recursive=True, include_folder=False) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py b/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py index cf987765f4..144df10fe0 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_layout_existing.py @@ -3,15 +3,14 @@ from pathlib import Path import unreal from unreal import EditorLevelLibrary +import ayon_api -from ayon_core.client import get_representations from ayon_core.pipeline import ( discover_loader_plugins, loaders_from_representation, load_container, get_representation_path, AYON_CONTAINER_ID, - get_current_project_name, ) from ayon_core.hosts.unreal.api import plugin from ayon_core.hosts.unreal.api import pipeline as upipeline @@ -43,11 +42,15 @@ class ExistingLayoutLoader(plugin.Loader): @staticmethod def _create_container( - asset_name, asset_dir, asset, representation, parent, family + asset_name, + asset_dir, + folder_path, + representation, + version_id, + product_type ): container_name = f"{asset_name}_CON" - container = None if not unreal.EditorAssetLibrary.does_asset_exist( f"{asset_dir}/{container_name}" ): @@ -61,14 +64,17 @@ class ExistingLayoutLoader(plugin.Loader): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, # "loader": str(self.__class__.__name__), "representation": representation, - "parent": parent, - "family": family + "parent": version_id, + "product_type": product_type, + # TODO these shold be probably removed + "asset": folder_path, + "family": product_type, } upipeline.imprint( @@ -195,19 +201,19 @@ class ExistingLayoutLoader(plugin.Loader): return assets - def _get_valid_repre_docs(self, project_name, version_ids): + def _get_valid_repre_entities(self, project_name, version_ids): valid_formats = ['fbx', 'abc'] - repre_docs = list(get_representations( + repre_entities = list(ayon_api.get_representations( project_name, representation_names=valid_formats, version_ids=version_ids )) - repre_doc_by_version_id = {} - for repre_doc in repre_docs: - version_id = str(repre_doc["parent"]) - repre_doc_by_version_id[version_id] = repre_doc - return repre_doc_by_version_id + repre_entities_by_version_id = {} + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + repre_entities_by_version_id[version_id] = repre_entity + return repre_entities_by_version_id def _process(self, lib_path, project_name): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -226,39 +232,52 @@ class ExistingLayoutLoader(plugin.Loader): repre_ids.add(repre_id) elements.append(element) - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_ids=repre_ids ) - repre_docs_by_id = { - str(repre_doc["_id"]): repre_doc - for repre_doc in repre_docs + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in repre_entities } layout_data = [] version_ids = set() for element in elements: repre_id = element.get("representation") - repre_doc = repre_docs_by_id.get(repre_id) - if not repre_doc: + repre_entity = repre_entities_by_id.get(repre_id) + if not repre_entity: raise AssertionError("Representation not found") - if not (repre_doc.get('data') or repre_doc['data'].get('path')): + if not ( + repre_entity.get("attrib") + or repre_entity["attrib"].get("path") + ): raise AssertionError("Representation does not have path") - if not repre_doc.get('context'): + if not repre_entity.get('context'): raise AssertionError("Representation does not have context") - layout_data.append((repre_doc, element)) - version_ids.add(repre_doc["parent"]) + layout_data.append((repre_entity, element)) + version_ids.add(repre_entity["versionId"]) + + repre_parents_by_id = ayon_api.get_representation_parents( + project_name, repre_entities_by_id.keys() + ) # Prequery valid repre documents for all elements at once - valid_repre_doc_by_version_id = self._get_valid_repre_docs( + valid_repre_entities_by_version_id = self._get_valid_repre_entities( project_name, version_ids) containers = [] actors_matched = [] - for (repr_data, lasset) in layout_data: + for (repre_entity, lasset) in layout_data: # For every actor in the scene, check if it has a representation in # those we got from the JSON. If so, create a container for it. # Otherwise, remove it from the scene. found = False + repre_id = repre_entity["id"] + repre_parents = repre_parents_by_id[repre_id] + folder_path = repre_parents.folder["path"] + folder_name = repre_parents.folder["name"] + product_name = repre_parents.product["name"] + product_type = repre_parents.product["productType"] for actor in actors: if not actor.get_class().get_name() == 'StaticMeshActor': @@ -275,7 +294,7 @@ class ExistingLayoutLoader(plugin.Loader): path = Path(filename) if (not path.name or - path.name not in repr_data.get('data').get('path')): + path.name not in repre_entity["attrib"]["path"]): continue actor.set_actor_label(lasset.get('instance_name')) @@ -283,12 +302,13 @@ class ExistingLayoutLoader(plugin.Loader): mesh_path = Path(mesh.get_path_name()).parent.as_posix() # Create the container for the asset. - asset = repr_data.get('context').get('asset') - product_name = repr_data.get('context').get('subset') container = self._create_container( - f"{asset}_{product_name}", mesh_path, asset, - repr_data.get('_id'), repr_data.get('parent'), - repr_data.get('context').get('family') + f"{folder_name}_{product_name}", + mesh_path, + folder_path, + repre_entity["id"], + repre_entity["versionId"], + product_type ) containers.append(container) @@ -316,18 +336,18 @@ class ExistingLayoutLoader(plugin.Loader): loaded = False for container in all_containers: - repr = container.get('representation') + repre_id = container.get('representation') - if not repr == str(repr_data.get('_id')): + if not repre_id == repre_entity["id"]: continue asset_dir = container.get('namespace') - filter = unreal.ARFilter( + arfilter = unreal.ARFilter( class_names=["StaticMesh"], package_paths=[asset_dir], recursive_paths=False) - assets = ar.get_assets(filter) + assets = ar.get_assets(arfilter) for asset in assets: obj = asset.get_asset() @@ -340,8 +360,9 @@ class ExistingLayoutLoader(plugin.Loader): if loaded: continue + version_id = lasset.get('version') assets = self._load_asset( - valid_repre_doc_by_version_id.get(lasset.get('version')), + valid_repre_entities_by_version_id.get(version_id), lasset.get('representation'), lasset.get('instance_name'), lasset.get('family') @@ -370,9 +391,11 @@ class ExistingLayoutLoader(plugin.Loader): def load(self, context, name, namespace, options): print("Loading Layout and Match Assets") - asset = context.get('asset').get('name') - asset_name = f"{asset}_{name}" if asset else name - container_name = f"{asset}_{name}_CON" + folder_name = context["folder"]["name"] + folder_path = context["folder"]["path"] + product_type = context["representation"]["context"]["family"] + asset_name = f"{folder_name}_{name}" if folder_name else name + container_name = f"{folder_name}_{name}_CON" curr_level = self._get_current_level() @@ -395,15 +418,18 @@ class ExistingLayoutLoader(plugin.Loader): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": curr_level_path, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"], - "loaded_assets": containers + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], + "product_type": product_type, + "loaded_assets": containers, + # TODO these shold be probably removed + "asset": folder_path, + "family": product_type, } upipeline.imprint(f"{curr_level_path}/{container_name}", data) @@ -411,15 +437,15 @@ class ExistingLayoutLoader(plugin.Loader): asset_dir = container.get('namespace') project_name = context["project"]["name"] - repre_doc = context["representation"] + repre_entity = context["representation"] - source_path = get_representation_path(repre_doc) + source_path = get_representation_path(repre_entity) containers = self._process(source_path, project_name) data = { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]), - "loaded_assets": containers + "representation": repre_entity["id"], + "loaded_assets": containers, + "parent": repre_entity["versionId"], } upipeline.imprint( "{}/{}".format(asset_dir, container.get('container_name')), data) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py index 58fbda491c..4d9c0554d8 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_abc.py @@ -73,19 +73,28 @@ class SkeletalMeshAlembicLoader(plugin.Loader): create_container(container=container_name, path=asset_dir) def imprint( - self, asset, asset_dir, container_name, asset_name, representation + self, + folder_path, + asset_dir, + container_name, + asset_name, + representation, + product_type ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": representation["_id"], - "parent": representation["parent"], - "family": representation["context"]["family"] + "representation": representation["id"], + "parent": representation["versionId"], + "product_type": product_type, + # TODO these should be probably removed + "asset": folder_path, + "family": product_type, } imprint(f"{asset_dir}/{container_name}", data) @@ -105,15 +114,16 @@ class SkeletalMeshAlembicLoader(plugin.Loader): list(str): list of container content """ # Create directory for asset and ayon container - asset = context.get('asset').get('name') + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version') + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" + version = context["version"]["version"] # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": + if version < 0: name_version = f"{name}_hero" else: - name_version = f"{name}_v{version.get('name'):03d}" + name_version = f"{name}_v{version:03d}" default_conversion = False if options.get("default_conversion"): @@ -121,7 +131,7 @@ class SkeletalMeshAlembicLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{asset}/{name_version}", suffix="") + f"{self.root}/{folder_name}/{name_version}", suffix="") container_name += suffix @@ -131,9 +141,15 @@ class SkeletalMeshAlembicLoader(plugin.Loader): self.import_and_containerize(path, asset_dir, asset_name, container_name, default_conversion) + product_type = context["product"]["productType"] self.imprint( - asset, asset_dir, container_name, asset_name, - context["representation"]) + folder_path, + asset_dir, + container_name, + asset_name, + context["representation"], + product_type + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -145,13 +161,12 @@ class SkeletalMeshAlembicLoader(plugin.Loader): return asset_content def update(self, container, context): - asset_doc = context["asset"] - subset_doc = context["subset"] - version_doc = context["version"] - repre_doc = context["representation"] - - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + product_type = context["product"]["productType"] + version = context["version"]["version"] + repre_entity = context["representation"] # Create directory for folder and Ayon container suffix = "_CON" @@ -159,7 +174,6 @@ class SkeletalMeshAlembicLoader(plugin.Loader): if folder_name: asset_name = f"{folder_name}_{product_name}" # Check if version is hero version and use different name - version = version_doc.get("name", -1) if version < 0: name_version = f"{product_name}_hero" else: @@ -171,13 +185,19 @@ class SkeletalMeshAlembicLoader(plugin.Loader): container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) self.import_and_containerize(path, asset_dir, asset_name, container_name) self.imprint( - folder_name, asset_dir, container_name, asset_name, repre_doc) + folder_path, + asset_dir, + container_name, + asset_name, + repre_entity, + product_type, + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py index 436d4c8a58..990ad4b977 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_skeletalmesh_fbx.py @@ -78,19 +78,28 @@ class SkeletalMeshFBXLoader(plugin.Loader): create_container(container=container_name, path=asset_dir) def imprint( - self, asset, asset_dir, container_name, asset_name, representation + self, + folder_path, + asset_dir, + container_name, + asset_name, + representation, + product_type ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": representation["_id"], - "parent": representation["parent"], - "family": representation["context"]["family"] + "representation": representation["id"], + "parent": representation["versionId"], + "product_type": product_type, + # TODO these should be probably removed + "asset": folder_path, + "family": product_type, } imprint(f"{asset_dir}/{container_name}", data) @@ -110,19 +119,21 @@ class SkeletalMeshFBXLoader(plugin.Loader): list(str): list of container content """ # Create directory for asset and Ayon container - asset = context.get('asset').get('name') + folder_name = context["folder"]["name"] + product_type = context["product"]["productType"] suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version') + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" + version_entity = context["version"] # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": + version = version_entity["version"] + if version < 0: name_version = f"{name}_hero" else: - name_version = f"{name}_v{version.get('name'):03d}" + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{asset}/{name_version}", suffix="" + f"{self.root}/{folder_name}/{name_version}", suffix="" ) container_name += suffix @@ -134,8 +145,13 @@ class SkeletalMeshFBXLoader(plugin.Loader): path, asset_dir, asset_name, container_name) self.imprint( - asset, asset_dir, container_name, asset_name, - context["representation"]) + folder_name, + asset_dir, + container_name, + asset_name, + context["representation"], + product_type + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -147,13 +163,12 @@ class SkeletalMeshFBXLoader(plugin.Loader): return asset_content def update(self, container, context): - asset_doc = context["asse"] - subset_doc = context["subset"] - version_doc = context["version"] - repre_doc = context["representation"] - - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + product_type = context["product"]["productType"] + version = context["version"]["version"] + repre_entity = context["representation"] # Create directory for asset and Ayon container suffix = "_CON" @@ -161,7 +176,6 @@ class SkeletalMeshFBXLoader(plugin.Loader): if folder_name: asset_name = f"{folder_name}_{product_name}" # Check if version is hero version and use different name - version = version_doc.get("name", -1) if version < 0: name_version = f"{product_name}_hero" else: @@ -173,13 +187,19 @@ class SkeletalMeshFBXLoader(plugin.Loader): container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) self.import_and_containerize( path, asset_dir, asset_name, container_name) self.imprint( - folder_name, asset_dir, container_name, asset_name, repre_doc) + folder_path, + asset_dir, + container_name, + asset_name, + repre_entity, + product_type + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py index 6ff4bcd4f2..5e4a28a031 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_abc.py @@ -74,19 +74,28 @@ class StaticMeshAlembicLoader(plugin.Loader): create_container(container=container_name, path=asset_dir) def imprint( - self, asset, asset_dir, container_name, asset_name, representation + self, + folder_path, + asset_dir, + container_name, + asset_name, + representation, + product_type, ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, + "folder_path": folder_path, "namespace": asset_dir, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": representation["_id"], - "parent": representation["parent"], - "family": representation["context"]["family"] + "representation": representation["id"], + "parent": representation["versionId"], + "product_type": product_type, + # TODO these should be probably removed + "asset": folder_path, + "family": product_type } imprint(f"{asset_dir}/{container_name}", data) @@ -106,15 +115,17 @@ class StaticMeshAlembicLoader(plugin.Loader): list(str): list of container content """ # Create directory for asset and Ayon container - asset = context.get('asset').get('name') + folder_path = context["folder"]["path"] + folder_name = context["folder"]["path"] + suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version') + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" + version = context["version"]["version"] # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": + if version < 0: name_version = f"{name}_hero" else: - name_version = f"{name}_v{version.get('name'):03d}" + name_version = f"{name}_v{version:03d}" default_conversion = False if options.get("default_conversion"): @@ -122,7 +133,7 @@ class StaticMeshAlembicLoader(plugin.Loader): tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{asset}/{name_version}", suffix="") + f"{self.root}/{folder_name}/{name_version}", suffix="") container_name += suffix @@ -132,9 +143,15 @@ class StaticMeshAlembicLoader(plugin.Loader): self.import_and_containerize(path, asset_dir, asset_name, container_name, default_conversion) + product_type = context["product"]["productType"] self.imprint( - asset, asset_dir, container_name, asset_name, - context["representation"]) + folder_path, + asset_dir, + container_name, + asset_name, + context["representation"], + product_type + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False @@ -146,21 +163,24 @@ class StaticMeshAlembicLoader(plugin.Loader): return asset_content def update(self, container, context): - asset_doc = context["asset"] - subset_doc = context["subset"] - repre_doc = context["representation"] - - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + product_type = context["product"]["productType"] + repre_entity = context["representation"] # Create directory for asset and Ayon container suffix = "_CON" asset_name = product_name if folder_name: asset_name = f"{folder_name}_{product_name}" - version = context.get('version') + version = context["version"]["version"] # Check if version is hero version and use different name - name_version = f"{product_name}_v{version:03d}" if version else f"{product_name}_hero" + if version < 0: + name_version = f"{product_name}_hero" + else: + name_version = f"{product_name}_v{version:03d}" + tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( f"{self.root}/{folder_name}/{name_version}", suffix="") @@ -168,13 +188,19 @@ class StaticMeshAlembicLoader(plugin.Loader): container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) self.import_and_containerize(path, asset_dir, asset_name, container_name) self.imprint( - folder_name, asset_dir, container_name, asset_name, repre_doc) + folder_path, + asset_dir, + container_name, + asset_name, + repre_entity, + product_type + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py index d2c6fc5566..1db2dcf676 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_staticmesh_fbx.py @@ -66,19 +66,28 @@ class StaticMeshFBXLoader(plugin.Loader): create_container(container=container_name, path=asset_dir) def imprint( - self, asset, asset_dir, container_name, asset_name, representation + self, + folder_path, + asset_dir, + container_name, + asset_name, + repre_entity, + product_type ): data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, "namespace": asset_dir, + "folder_path": folder_path, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": representation["_id"], - "parent": representation["parent"], - "family": representation["context"]["family"] + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], + "product_type": product_type, + # TODO these shold be probably removed + "asset": folder_path, + "family": product_type, } imprint(f"{asset_dir}/{container_name}", data) @@ -98,19 +107,20 @@ class StaticMeshFBXLoader(plugin.Loader): list(str): list of container content """ # Create directory for asset and Ayon container - asset = context.get('asset').get('name') + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" - version = context.get('version') + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" + version = context["version"]["version"] # Check if version is hero version and use different name - if not version.get("name") and version.get('type') == "hero_version": + if version < 0: name_version = f"{name}_hero" else: - name_version = f"{name}_v{version.get('name'):03d}" + name_version = f"{name}_v{version:03d}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{self.root}/{asset}/{name_version}", suffix="" + f"{self.root}/{folder_name}/{name_version}", suffix="" ) container_name += suffix @@ -122,8 +132,13 @@ class StaticMeshFBXLoader(plugin.Loader): path, asset_dir, asset_name, container_name) self.imprint( - asset, asset_dir, container_name, asset_name, - context["representation"]) + folder_path, + asset_dir, + container_name, + asset_name, + context["representation"], + context["product"]["productType"] + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=True @@ -135,13 +150,12 @@ class StaticMeshFBXLoader(plugin.Loader): return asset_content def update(self, container, context): - asset_doc = context["asset"] - subset_doc = context["subset"] - version_doc = context["version"] - repre_doc = context["representation"] - - folder_name = asset_doc["name"] - product_name = subset_doc["name"] + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] + product_name = context["product"]["name"] + product_type = context["product"]["productType"] + version = context["version"]["version"] + repre_entity = context["representation"] # Create directory for asset and Ayon container suffix = "_CON" @@ -149,7 +163,6 @@ class StaticMeshFBXLoader(plugin.Loader): if folder_name: asset_name = f"{folder_name}_{product_name}" # Check if version is hero version and use different name - version = version_doc.get("name", -1) if version < 0: name_version = f"{product_name}_hero" else: @@ -161,13 +174,19 @@ class StaticMeshFBXLoader(plugin.Loader): container_name += suffix if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir): - path = get_representation_path(repre_doc) + path = get_representation_path(repre_entity) self.import_and_containerize( path, asset_dir, asset_name, container_name) self.imprint( - folder_name, asset_dir, container_name, asset_name, repre_doc) + folder_path, + asset_dir, + container_name, + asset_name, + repre_entity, + product_type, + ) asset_content = unreal.EditorAssetLibrary.list_assets( asset_dir, recursive=True, include_folder=False diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py b/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py index 9710d213ee..e0b1a69aac 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_uasset.py @@ -42,12 +42,13 @@ class UAssetLoader(plugin.Loader): # Create directory for asset and Ayon container root = unreal_pipeline.AYON_ASSET_DIR - asset = context.get('asset').get('name') + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}", suffix="" + f"{root}/{folder_name}/{name}", suffix="" ) unique_number = 1 @@ -73,17 +74,21 @@ class UAssetLoader(plugin.Loader): unreal_pipeline.create_container( container=container_name, path=asset_dir) + product_type = context["product"]["productType"] data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, "namespace": asset_dir, + "folder_path": folder_path, "container_name": container_name, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"], + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], + "product_type": product_type, + # TODO these should be probably removed + "asset": folder_path, + "family": product_type, } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -101,10 +106,8 @@ class UAssetLoader(plugin.Loader): asset_dir = container["namespace"] - subset_doc = context["subset"] - repre_doc = context["representation"] - - product_name = subset_doc["name"] + product_name = context["product"]["name"] + repre_entity = context["representation"] unique_number = container["container_name"].split("_")[-2] @@ -120,7 +123,7 @@ class UAssetLoader(plugin.Loader): if obj.get_class().get_name() != "AyonAssetContainer": unreal.EditorAssetLibrary.delete_asset(asset) - update_filepath = get_representation_path(repre_doc) + update_filepath = get_representation_path(repre_entity) shutil.copy( update_filepath, @@ -132,8 +135,8 @@ class UAssetLoader(plugin.Loader): unreal_pipeline.imprint( container_path, { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]), + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], } ) diff --git a/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py b/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py index c6e275c844..7a1767e52a 100644 --- a/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py +++ b/client/ayon_core/hosts/unreal/plugins/load/load_yeticache.py @@ -87,13 +87,14 @@ class YetiLoader(plugin.Loader): # Create directory for asset and Ayon container root = unreal_pipeline.AYON_ASSET_DIR - asset = context.get('asset').get('name') + folder_path = context["folder"]["path"] + folder_name = context["folder"]["name"] suffix = "_CON" - asset_name = f"{asset}_{name}" if asset else f"{name}" + asset_name = f"{folder_name}_{name}" if folder_name else f"{name}" tools = unreal.AssetToolsHelpers().get_asset_tools() asset_dir, container_name = tools.create_unique_asset_name( - f"{root}/{asset}/{name}", suffix="") + f"{root}/{folder_name}/{name}", suffix="") unique_number = 1 while unreal.EditorAssetLibrary.does_directory_exist( @@ -116,17 +117,21 @@ class YetiLoader(plugin.Loader): unreal_pipeline.create_container( container=container_name, path=asset_dir) + product_type = context["product"]["productType"] data = { "schema": "ayon:container-2.0", "id": AYON_CONTAINER_ID, - "asset": asset, "namespace": asset_dir, "container_name": container_name, + "folder_path": folder_path, "asset_name": asset_name, "loader": str(self.__class__.__name__), - "representation": context["representation"]["_id"], - "parent": context["representation"]["parent"], - "family": context["representation"]["context"]["family"] + "representation": context["representation"]["id"], + "parent": context["representation"]["versionId"], + "product_type": product_type, + # TODO these shold be probably removed + "asset": folder_path, + "family": product_type, } unreal_pipeline.imprint(f"{asset_dir}/{container_name}", data) @@ -140,9 +145,9 @@ class YetiLoader(plugin.Loader): return asset_content def update(self, container, context): - repre_doc = context["representation"] + repre_entity = context["representation"] name = container["asset_name"] - source_path = get_representation_path(repre_doc) + source_path = get_representation_path(repre_entity) destination_path = container["namespace"] task = self.get_task(source_path, destination_path, name, True) @@ -155,8 +160,8 @@ class YetiLoader(plugin.Loader): unreal_pipeline.imprint( container_path, { - "representation": str(repre_doc["_id"]), - "parent": str(repre_doc["parent"]) + "representation": repre_entity["id"], + "parent": repre_entity["versionId"], }) asset_content = unreal.EditorAssetLibrary.list_assets( diff --git a/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py b/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py index de8cf0be2a..5489057021 100644 --- a/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py +++ b/client/ayon_core/hosts/unreal/plugins/publish/extract_layout.py @@ -6,8 +6,8 @@ import math import unreal from unreal import EditorLevelLibrary as ell from unreal import EditorAssetLibrary as eal +import ayon_api -from ayon_core.client import get_representation_by_name from ayon_core.pipeline import publish @@ -60,10 +60,10 @@ class ExtractLayout(publish.Extractor): family = eal.get_metadata_tag(asset_container, "family") self.log.info("Parent: {}".format(parent_id)) - blend = get_representation_by_name( - project_name, "blend", parent_id, fields=["_id"] + blend = ayon_api.get_representation_by_name( + project_name, "blend", parent_id, fields={"id"} ) - blend_id = blend["_id"] + blend_id = blend["id"] json_element = {} json_element["reference"] = str(blend_id) diff --git a/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py b/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py index 205436ad37..85214a2b0d 100644 --- a/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py +++ b/client/ayon_core/hosts/unreal/plugins/publish/validate_sequence_frames.py @@ -22,8 +22,12 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): def process(self, instance): representations = instance.data.get("representations") + folder_attributes = ( + instance.data + .get("folderEntity", {}) + .get("attrib", {}) + ) for repr in representations: - data = instance.data.get("assetEntity", {}).get("data", {}) repr_files = repr["files"] if isinstance(repr_files, str): continue @@ -64,8 +68,8 @@ class ValidateSequenceFrames(pyblish.api.InstancePlugin): frames = frames[1:] current_range = (frames[0], frames[-1]) - required_range = (data["clipIn"], - data["clipOut"]) + required_range = (folder_attributes["clipIn"], + folder_attributes["clipOut"]) if current_range != required_range: raise PublishValidationError( diff --git a/client/ayon_core/lib/__init__.py b/client/ayon_core/lib/__init__.py index acd960d9b5..f69fd10b07 100644 --- a/client/ayon_core/lib/__init__.py +++ b/client/ayon_core/lib/__init__.py @@ -15,7 +15,18 @@ python_version_dir = os.path.join( sys.path.insert(0, python_version_dir) site.addsitedir(python_version_dir) - +from .local_settings import ( + IniSettingRegistry, + JSONSettingRegistry, + AYONSecureRegistry, + AYONSettingsRegistry, + OpenPypeSecureRegistry, + OpenPypeSettingsRegistry, + get_local_site_id, + get_ayon_username, + get_openpype_username, +) +from .ayon_connection import initialize_ayon_connection from .events import ( emit_event, register_event_callback @@ -112,18 +123,6 @@ from .transcoding import ( get_rescaled_command_arguments, ) -from .local_settings import ( - IniSettingRegistry, - JSONSettingRegistry, - AYONSecureRegistry, - AYONSettingsRegistry, - OpenPypeSecureRegistry, - OpenPypeSettingsRegistry, - get_local_site_id, - get_ayon_username, - get_openpype_username, -) - from .applications import ( ApplicationLaunchFailed, ApplictionExecutableNotFound, @@ -164,6 +163,18 @@ from .ayon_info import ( terminal = Terminal __all__ = [ + "IniSettingRegistry", + "JSONSettingRegistry", + "AYONSecureRegistry", + "AYONSettingsRegistry", + "OpenPypeSecureRegistry", + "OpenPypeSettingsRegistry", + "get_local_site_id", + "get_ayon_username", + "get_openpype_username", + + "initialize_ayon_connection", + "emit_event", "register_event_callback", @@ -222,16 +233,6 @@ __all__ = [ "convert_ffprobe_fps_to_float", "get_rescaled_command_arguments", - "IniSettingRegistry", - "JSONSettingRegistry", - "AYONSecureRegistry", - "AYONSettingsRegistry", - "OpenPypeSecureRegistry", - "OpenPypeSettingsRegistry", - "get_local_site_id", - "get_ayon_username", - "get_openpype_username", - "ApplicationLaunchFailed", "ApplictionExecutableNotFound", "ApplicationNotFound", diff --git a/client/ayon_core/lib/applications.py b/client/ayon_core/lib/applications.py index 8f1a1d10ea..4bf0c31d93 100644 --- a/client/ayon_core/lib/applications.py +++ b/client/ayon_core/lib/applications.py @@ -12,7 +12,6 @@ from abc import ABCMeta, abstractmethod import six from ayon_core import AYON_CORE_ROOT -from ayon_core.client import get_asset_name_identifier from ayon_core.settings import get_project_settings, get_studio_settings from .log import Logger from .profiles_filtering import filter_profiles @@ -1381,7 +1380,7 @@ class EnvironmentPrepData(dict): data (dict): Data must contain required keys. """ required_keys = ( - "project_doc", "asset_doc", "task_name", "app", "anatomy" + "project_entity", "folder_entity", "task_entity", "app", "anatomy" ) def __init__(self, data): @@ -1395,7 +1394,7 @@ class EnvironmentPrepData(dict): if data.get("env") is None: data["env"] = os.environ.copy() - project_name = data["project_doc"]["name"] + project_name = data["project_entity"]["name"] if "project_settings" not in data: data["project_settings"] = get_project_settings(project_name) @@ -1546,13 +1545,13 @@ def prepare_app_environments( app.environment ] - asset_doc = data.get("asset_doc") + folder_entity = data.get("folder_entity") # Add tools environments groups_by_name = {} tool_by_group_name = collections.defaultdict(dict) - if asset_doc: + if folder_entity: # Make sure each tool group can be added only once - for key in asset_doc["data"].get("tools_env") or []: + for key in folder_entity["attrib"].get("tools") or []: tool = app.manager.tools.get(key) if not tool or not tool.is_valid_for_app(app): continue @@ -1674,10 +1673,10 @@ def prepare_context_environments(data, env_group=None, addons_manager=None): # Context environments log = data["log"] - project_doc = data["project_doc"] - asset_doc = data["asset_doc"] - task_name = data["task_name"] - if not project_doc: + project_entity = data["project_entity"] + folder_entity = data["folder_entity"] + task_entity = data["task_entity"] + if not project_entity: log.info( "Skipping context environments preparation." " Launch context does not contain required data." @@ -1685,21 +1684,21 @@ def prepare_context_environments(data, env_group=None, addons_manager=None): return # Load project specific environments - project_name = project_doc["name"] + project_name = project_entity["name"] project_settings = get_project_settings(project_name) data["project_settings"] = project_settings app = data["app"] context_env = { - "AYON_PROJECT_NAME": project_doc["name"], + "AYON_PROJECT_NAME": project_entity["name"], "AYON_APP_NAME": app.full_name } - if asset_doc: - asset_name = get_asset_name_identifier(asset_doc) - context_env["AYON_FOLDER_PATH"] = asset_name + if folder_entity: + folder_path = folder_entity["path"] + context_env["AYON_FOLDER_PATH"] = folder_path - if task_name: - context_env["AYON_TASK_NAME"] = task_name + if task_entity: + context_env["AYON_TASK_NAME"] = task_entity["name"] log.debug( "Context environments set:\n{}".format( @@ -1719,15 +1718,19 @@ def prepare_context_environments(data, env_group=None, addons_manager=None): data["env"]["AYON_HOST_NAME"] = app.host_name - if not asset_doc or not task_name: + if not folder_entity or not task_entity: # QUESTION replace with log.info and skip workfile discovery? # - technically it should be possible to launch host without context raise ApplicationLaunchFailed( - "Host launch require asset and task context." + "Host launch require folder and task context." ) workdir_data = get_template_data( - project_doc, asset_doc, task_name, app.host_name, project_settings + project_entity, + folder_entity, + task_entity, + app.host_name, + project_settings ) data["workdir_data"] = workdir_data @@ -1853,9 +1856,9 @@ def _prepare_last_workfile(data, workdir, addons_manager): project_settings = data["project_settings"] task_type = workdir_data["task"]["type"] template_key = get_workfile_template_key( + project_name, task_type, app.host_name, - project_name, project_settings=project_settings ) # Find last workfile diff --git a/client/ayon_core/lib/ayon_connection.py b/client/ayon_core/lib/ayon_connection.py new file mode 100644 index 0000000000..5ee654943f --- /dev/null +++ b/client/ayon_core/lib/ayon_connection.py @@ -0,0 +1,97 @@ +import os + +import semver +import ayon_api + +from .local_settings import get_local_site_id + + +class _Cache: + initialized = False + + +def _new_get_last_version_by_product_name( + self, + project_name, + product_name, + folder_id, + active=True, + fields=None, + own_attributes=False +): + """Query last version entity by product name and folder id. + + Args: + project_name (str): Project where to look for representation. + product_name (str): Product name. + folder_id (str): Folder id. + active (Optional[bool]): Receive active/inactive entities. + Both are returned when 'None' is passed. + fields (Optional[Iterable[str]]): fields to be queried + for representations. + own_attributes (Optional[bool]): Attribute values that are + not explicitly set on entity will have 'None' value. + + Returns: + Union[dict[str, Any], None]: Queried version entity or None. + + """ + if not folder_id: + return None + + product = self.get_product_by_name( + project_name, product_name, folder_id, fields={"id"} + ) + if not product: + return None + return self.get_last_version_by_product_id( + project_name, + product["id"], + active=active, + fields=fields, + own_attributes=own_attributes + ) + + +def initialize_ayon_connection(force=False): + """Initialize global AYON api connection. + + Create global connection in ayon_api module and set site id + and client version. + Is silently skipped if already happened. + + Args: + force (Optional[bool]): Force reinitialize connection. + Defaults to False. + + """ + if not force and _Cache.initialized: + return + + _Cache.initialized = True + ayon_api_version = ( + semver.VersionInfo.parse(ayon_api.__version__).to_tuple() + ) + # TODO remove mokey patching after when AYON api is safely updated + fix_last_version_by_product_name = ayon_api_version < (1, 0, 2) + # Monkey patching to fix 'get_last_version_by_product_name' + if fix_last_version_by_product_name: + ayon_api.ServerAPI.get_last_version_by_product_name = ( + _new_get_last_version_by_product_name + ) + + site_id = get_local_site_id() + version = os.getenv("AYON_VERSION") + if ayon_api.is_connection_created(): + con = ayon_api.get_server_api_connection() + # Monkey patching to fix 'get_last_version_by_product_name' + if fix_last_version_by_product_name: + def _con_wrapper(*args, **kwargs): + return _new_get_last_version_by_product_name( + con, *args, **kwargs + ) + con.get_last_version_by_product_name = _con_wrapper + con.set_site_id(site_id) + con.set_client_version(version) + else: + ayon_api.create_connection(site_id, version) diff --git a/client/ayon_core/lib/local_settings.py b/client/ayon_core/lib/local_settings.py index 022f63a618..9eba3d1ed1 100644 --- a/client/ayon_core/lib/local_settings.py +++ b/client/ayon_core/lib/local_settings.py @@ -26,8 +26,7 @@ except ImportError: import six import appdirs - -from ayon_core.client import get_ayon_server_api_connection +import ayon_api _PLACEHOLDER = object() @@ -556,10 +555,9 @@ def get_ayon_username(): Returns: str: Username. - """ - con = get_ayon_server_api_connection() - return con.get_user()["name"] + """ + return ayon_api.get_user()["name"] def get_openpype_username(): diff --git a/client/ayon_core/lib/path_tools.py b/client/ayon_core/lib/path_tools.py index fec6a0c47d..a65f0f8e13 100644 --- a/client/ayon_core/lib/path_tools.py +++ b/client/ayon_core/lib/path_tools.py @@ -78,7 +78,7 @@ def collect_frames(files): files(list) or (set with single value): list of source paths Returns: - (dict): {'/asset/subset_v001.0001.png': '0001', ....} + dict: {'/folder/product_v001.0001.png': '0001', ....} """ patterns = [clique.PATTERNS["frames"]] diff --git a/client/ayon_core/modules/clockify/clockify_module.py b/client/ayon_core/modules/clockify/clockify_module.py index 58407bfe94..d2ee4f1e1e 100644 --- a/client/ayon_core/modules/clockify/clockify_module.py +++ b/client/ayon_core/modules/clockify/clockify_module.py @@ -3,7 +3,6 @@ import threading import time from ayon_core.modules import AYONAddon, ITrayModule, IPluginPaths -from ayon_core.client import get_asset_by_name from .constants import CLOCKIFY_FTRACK_USER_PATH, CLOCKIFY_FTRACK_SERVER_PATH @@ -255,33 +254,27 @@ class ClockifyModule(AYONAddon, ITrayModule, IPluginPaths): if not self.clockify_api.get_api_key(): return + project_name = input_data.get("project_name") + folder_path = input_data.get("folder_path") task_name = input_data.get("task_name") + task_type = input_data.get("task_type") + if not all((project_name, folder_path, task_name, task_type)): + return # Concatenate hierarchy and task to get description - description_items = list(input_data.get("hierarchy", [])) - description_items.append(task_name) - description = "/".join(description_items) + description = "/".join([folder_path.lstrip("/"), task_name]) # Check project existence - project_name = input_data.get("project_name") project_id = self._verify_project_exists(project_name) if not project_id: return # Setup timer tags - tag_ids = [] - tag_name = input_data.get("task_type") - if not tag_name: - # no task_type found in the input data - # if the timer is restarted by idle time (bug?) - asset_name = input_data["hierarchy"][-1] - asset_doc = get_asset_by_name(project_name, asset_name) - task_info = asset_doc["data"]["tasks"][task_name] - tag_name = task_info.get("type", "") - if not tag_name: - self.log.info("No tag information found for the timer") + if not task_type: + self.log.info("No tag information found for the timer") - task_tag_id = self.clockify_api.get_tag_id(tag_name) + tag_ids = [] + task_tag_id = self.clockify_api.get_tag_id(task_type) if task_tag_id is not None: tag_ids.append(task_tag_id) diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py b/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py index f7dd1772b0..61c5eac2f5 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py +++ b/client/ayon_core/modules/clockify/launcher_actions/ClockifyStart.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_asset_by_name +import ayon_api + from ayon_core.pipeline import LauncherAction from openpype_modules.clockify.clockify_api import ClockifyAPI @@ -21,24 +22,18 @@ class ClockifyStart(LauncherAction): user_id = self.clockify_api.user_id workspace_id = self.clockify_api.workspace_id project_name = session["AYON_PROJECT_NAME"] - asset_name = session["AYON_FOLDER_PATH"] + folder_path = session["AYON_FOLDER_PATH"] task_name = session["AYON_TASK_NAME"] - description = asset_name + description = "/".join([folder_path.lstrip("/"), task_name]) - # fetch asset docs - asset_doc = get_asset_by_name(project_name, asset_name) + # fetch folder entity + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) # get task type to fill the timer tag - task_info = asset_doc["data"]["tasks"][task_name] - task_type = task_info["type"] - - # check if the task has hierarchy and fill the - parents_data = asset_doc["data"] - if parents_data is not None: - description_items = parents_data.get("parents", []) - description_items.append(asset_name) - description_items.append(task_name) - description = "/".join(description_items) + task_type = task_entity["taskType"] project_id = self.clockify_api.get_project_id( project_name, workspace_id diff --git a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py b/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py index 5ef9033ffe..72187c6d28 100644 --- a/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py +++ b/client/ayon_core/modules/clockify/launcher_actions/ClockifySync.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_projects, get_project +import ayon_api + from openpype_modules.clockify.clockify_api import ClockifyAPI from ayon_core.pipeline import LauncherAction @@ -21,7 +22,7 @@ class ClockifySync(LauncherAction): def is_compatible(self, session): """Check if there's some projects to sync""" try: - next(get_projects()) + next(ayon_api.get_projects()) return True except StopIteration: return False @@ -38,16 +39,18 @@ class ClockifySync(LauncherAction): ) project_name = session.get("AYON_PROJECT_NAME") or "" - projects_to_sync = [] if project_name.strip(): - projects_to_sync = [get_project(project_name)] + projects_to_sync = [ayon_api.get_project(project_name)] else: - projects_to_sync = get_projects() + projects_to_sync = ayon_api.get_projects() - projects_info = {} - for project in projects_to_sync: - task_types = project["config"]["tasks"].keys() - projects_info[project["name"]] = task_types + projects_info = { + project["name"]: { + task_type["name"] + for task_type in project["taskTypes"] + } + for project in projects_to_sync + } clockify_projects = self.clockify_api.get_projects(workspace_id) for project_name, task_types in projects_info.items(): diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py index 3e95049e56..0561e0f65c 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_cache_job.py @@ -4,13 +4,11 @@ import os import json import re from copy import deepcopy -import requests +import requests +import ayon_api import pyblish.api -from ayon_core.client import ( - get_last_version_by_subset_name, -) from ayon_core.pipeline import publish from ayon_core.lib import EnumDef, is_in_tests from ayon_core.pipeline.version_start import get_versioning_start @@ -112,7 +110,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, output_dir = self._get_publish_folder( anatomy, deepcopy(instance.data["anatomyData"]), - instance.data.get("folderPath"), + instance.data.get("folderEntity"), instance.data["productName"], instance.context, instance.data["productType"], @@ -382,7 +380,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, json.dump(publish_job, f, indent=4, sort_keys=True) def _get_publish_folder(self, anatomy, template_data, - asset, product_name, context, + folder_entity, product_name, context, product_type, version=None): """ Extracted logic to pre-calculate real publish folder, which is @@ -396,7 +394,7 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, Args: anatomy (ayon_core.pipeline.anatomy.Anatomy): template_data (dict): pre-calculated collected data for process - asset (str): asset name + folder_entity (dict[str, Any]): Folder entity. product_name (str): Product name (actually group name of product). product_type (str): for current deadline process it's always 'render' @@ -411,18 +409,22 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, """ project_name = context.data["projectName"] + host_name = context.data["hostName"] if not version: - version = get_last_version_by_subset_name( - project_name, - product_name, - asset_name=asset - ) - if version: - version = int(version["name"]) + 1 + version_entity = None + if folder_entity: + version_entity = ayon_api.get_last_version_by_product_name( + project_name, + product_name, + folder_entity["id"] + ) + + if version_entity: + version = int(version_entity["version"]) + 1 else: version = get_versioning_start( project_name, - template_data["app"], + host_name, task_name=template_data["task"]["name"], task_type=template_data["task"]["type"], product_type="render", @@ -430,7 +432,6 @@ class ProcessSubmittedCacheJobOnFarm(pyblish.api.InstancePlugin, project_settings=context.data["project_settings"] ) - host_name = context.data["hostName"] task_info = template_data.get("task") or {} template_name = publish.get_publish_template_name( diff --git a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py index 7bc13ae4b6..7a6abd5507 100644 --- a/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py +++ b/client/ayon_core/modules/deadline/plugins/publish/submit_publish_job.py @@ -4,14 +4,12 @@ import os import json import re from copy import deepcopy + import requests import clique - +import ayon_api import pyblish.api -from ayon_core.client import ( - get_last_version_by_subset_name, -) from ayon_core.pipeline import publish from ayon_core.lib import EnumDef, is_in_tests from ayon_core.pipeline.version_start import get_versioning_start @@ -189,7 +187,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, output_dir = self._get_publish_folder( anatomy, deepcopy(instance.data["anatomyData"]), - instance.data.get("folderPath"), + instance.data.get("folderEntity"), instances[0]["productName"], instance.context, instances[0]["productType"], @@ -503,7 +501,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, json.dump(publish_job, f, indent=4, sort_keys=True) def _get_publish_folder(self, anatomy, template_data, - asset, product_name, context, + folder_entity, product_name, context, product_type, version=None): """ Extracted logic to pre-calculate real publish folder, which is @@ -517,7 +515,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, Args: anatomy (ayon_core.pipeline.anatomy.Anatomy): template_data (dict): pre-calculated collected data for process - asset (string): asset name + folder_entity (dict[str, Any]): Folder entity. product_name (string): Product name (actually group name of product) product_type (string): for current deadline process it's always @@ -535,13 +533,16 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin, project_name = context.data["projectName"] host_name = context.data["hostName"] if not version: - version = get_last_version_by_subset_name( - project_name, - product_name, - asset_name=asset - ) - if version: - version = int(version["name"]) + 1 + version_entity = None + if folder_entity: + version_entity = ayon_api.get_last_version_by_product_name( + project_name, + product_name, + folder_entity["id"] + ) + + if version_entity: + version = int(version_entity["version"]) + 1 else: version = get_versioning_start( project_name, diff --git a/client/ayon_core/modules/royalrender/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py b/client/ayon_core/modules/royalrender/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py index 778052778f..8405f69b3e 100644 --- a/client/ayon_core/modules/royalrender/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py +++ b/client/ayon_core/modules/royalrender/rr_root/plugins/control_job/perjob/m50__openpype_publish_render.py @@ -109,7 +109,7 @@ class OpenPypeContextSelector: if not self.context or \ not self.context.get("project") or \ - not self.context.get("asset") or \ + not self.context.get("folder") or \ not self.context.get("task"): self._show_rr_warning("Context selection failed.") return False @@ -137,7 +137,7 @@ class OpenPypeContextSelector: def run_publish(self): """Run publish process.""" env = {"AYON_PROJECT_NAME": str(self.context.get("project")), - "AYON_FOLDER_PATH": str(self.context.get("asset")), + "AYON_FOLDER_PATH": str(self.context.get("folder")), "AYON_TASK_NAME": str(self.context.get("task")), # "AYON_APP_NAME": str(self.context.get("app_name")) } @@ -184,7 +184,7 @@ selector = OpenPypeContextSelector() # try to set context from environment for key, env_keys in ( ("project", ["AYON_PROJECT_NAME", "AVALON_PROJECT"]), - ("asset", ["AYON_FOLDER_PATH", "AVALON_ASSET"]), + ("folder", ["AYON_FOLDER_PATH", "AVALON_ASSET"]), ("task", ["AYON_TASK_NAME", "AVALON_TASK"]), # ("app_name", ["AYON_APP_NAME", "AVALON_APP_NAME"]) ): diff --git a/client/ayon_core/modules/timers_manager/launch_hooks/post_start_timer.py b/client/ayon_core/modules/timers_manager/launch_hooks/post_start_timer.py index 4e94603aa9..da5d430939 100644 --- a/client/ayon_core/modules/timers_manager/launch_hooks/post_start_timer.py +++ b/client/ayon_core/modules/timers_manager/launch_hooks/post_start_timer.py @@ -11,13 +11,13 @@ class PostStartTimerHook(PostLaunchHook): def execute(self): project_name = self.data.get("project_name") - asset_name = self.data.get("folder_path") + folder_path = self.data.get("folder_path") task_name = self.data.get("task_name") missing_context_keys = set() if not project_name: missing_context_keys.add("project_name") - if not asset_name: + if not folder_path: missing_context_keys.add("folder_path") if not task_name: missing_context_keys.add("task_name") @@ -40,5 +40,5 @@ class PostStartTimerHook(PostLaunchHook): return timers_manager.start_timer_with_webserver( - project_name, asset_name, task_name, logger=self.log + project_name, folder_path, task_name, logger=self.log ) diff --git a/client/ayon_core/modules/timers_manager/plugins/publish/start_timer.py b/client/ayon_core/modules/timers_manager/plugins/publish/start_timer.py index 182efbc4ae..620cdb6e65 100644 --- a/client/ayon_core/modules/timers_manager/plugins/publish/start_timer.py +++ b/client/ayon_core/modules/timers_manager/plugins/publish/start_timer.py @@ -24,14 +24,14 @@ class StartTimer(pyblish.api.ContextPlugin): return project_name = context.data["projectName"] - asset_name = context.data.get("folderPath") + folder_path = context.data.get("folderPath") task_name = context.data.get("task") - if not project_name or not asset_name or not task_name: + if not project_name or not folder_path or not task_name: self.log.info(( "Current context does not contain all" " required information to start a timer." )) return timers_manager.start_timer_with_webserver( - project_name, asset_name, task_name, self.log + project_name, folder_path, task_name, self.log ) diff --git a/client/ayon_core/modules/timers_manager/rest_api.py b/client/ayon_core/modules/timers_manager/rest_api.py index c890d587de..88a6539510 100644 --- a/client/ayon_core/modules/timers_manager/rest_api.py +++ b/client/ayon_core/modules/timers_manager/rest_api.py @@ -45,7 +45,7 @@ class TimersManagerModuleRestApi: data = await request.json() try: project_name = data["project_name"] - asset_name = data["folder_path"] + folder_path = data["folder_path"] task_name = data["task_name"] except KeyError: msg = ( @@ -57,7 +57,7 @@ class TimersManagerModuleRestApi: self.module.stop_timers() try: - self.module.start_timer(project_name, asset_name, task_name) + self.module.start_timer(project_name, folder_path, task_name) except Exception as exc: return Response(status=404, message=str(exc)) @@ -70,9 +70,9 @@ class TimersManagerModuleRestApi: async def get_task_time(self, request): data = await request.json() try: - project_name = data['project_name'] - asset_name = data['folder_path'] - task_name = data['task_name'] + project_name = data["project_name"] + folder_path = data["folder_path"] + task_name = data["task_name"] except KeyError: message = ( "Payload must contain fields 'project_name, 'folder_path'," @@ -81,5 +81,5 @@ class TimersManagerModuleRestApi: self.log.warning(message) return Response(text=message, status=404) - time = self.module.get_task_time(project_name, asset_name, task_name) + time = self.module.get_task_time(project_name, folder_path, task_name) return Response(text=json.dumps(time)) diff --git a/client/ayon_core/modules/timers_manager/timers_manager.py b/client/ayon_core/modules/timers_manager/timers_manager.py index e04200525a..4212ff6b25 100644 --- a/client/ayon_core/modules/timers_manager/timers_manager.py +++ b/client/ayon_core/modules/timers_manager/timers_manager.py @@ -1,8 +1,8 @@ import os import platform +import ayon_api -from ayon_core.client import get_asset_by_name from ayon_core.addon import ( AYONAddon, ITrayService, @@ -24,14 +24,18 @@ class ExampleTimersManagerConnector: Required methods are 'stop_timer' and 'start_timer'. - # TODO pass asset document instead of `hierarchy` Example of `data` that are passed during changing timer: ``` data = { "project_name": project_name, + "folder_id": folder_id, + "folder_path": folder_entity["path"], "task_name": task_name, "task_type": task_type, - "hierarchy": hierarchy + # Deprecated + "asset_id": folder_id, + "asset_name": folder_entity["name"], + "hierarchy": hierarchy_items, } ``` """ @@ -176,16 +180,14 @@ class TimersManager( """Convert string path to a timer data. It is expected that first item is project name, last item is task name - and parent asset name is before task name. + and folder path in the middle. """ path_items = task_path.split("/") - if len(path_items) < 3: - raise InvalidContextError("Invalid path \"{}\"".format(task_path)) task_name = path_items.pop(-1) - asset_name = path_items.pop(-1) project_name = path_items.pop(0) + folder_path = "/" + "/".join(path_items) return self.get_timer_data_for_context( - project_name, asset_name, task_name, self.log + project_name, folder_path, task_name, self.log ) def get_launch_hook_paths(self): @@ -204,40 +206,38 @@ class TimersManager( @staticmethod def get_timer_data_for_context( - project_name, asset_name, task_name, logger=None + project_name, folder_path, task_name, logger=None ): - """Prepare data for timer related callbacks. - - TODO: - - return predefined object that has access to asset document etc. - """ - if not project_name or not asset_name or not task_name: + """Prepare data for timer related callbacks.""" + if not project_name or not folder_path or not task_name: raise InvalidContextError(( "Missing context information got" - " Project: \"{}\" Asset: \"{}\" Task: \"{}\"" - ).format(str(project_name), str(asset_name), str(task_name))) + " Project: \"{}\" Folder: \"{}\" Task: \"{}\"" + ).format(str(project_name), str(folder_path), str(task_name))) - asset_doc = get_asset_by_name( + folder_entity = ayon_api.get_folder_by_path( project_name, - asset_name, - fields=["_id", "name", "data.tasks", "data.parents"] + folder_path, + fields={"id", "name", "path"} ) - if not asset_doc: + if not folder_entity: raise InvalidContextError(( - "Asset \"{}\" not found in project \"{}\"" - ).format(asset_name, project_name)) + "Folder \"{}\" not found in project \"{}\"" + ).format(folder_path, project_name)) - asset_data = asset_doc.get("data") or {} - asset_tasks = asset_data.get("tasks") or {} - if task_name not in asset_tasks: + folder_id = folder_entity["id"] + task_entity = ayon_api.get_task_by_name( + project_name, folder_id, task_name + ) + if not task_entity: raise InvalidContextError(( - "Task \"{}\" not found on asset \"{}\" in project \"{}\"" - ).format(task_name, asset_name, project_name)) + "Task \"{}\" not found on folder \"{}\" in project \"{}\"" + ).format(task_name, folder_path, project_name)) task_type = "" try: - task_type = asset_tasks[task_name]["type"] + task_type = task_entity["taskType"] except KeyError: msg = "Couldn't find task_type for {}".format(task_name) if logger is not None: @@ -245,32 +245,34 @@ class TimersManager( else: print(msg) - hierarchy_items = asset_data.get("parents") or [] - hierarchy_items.append(asset_name) + hierarchy_items = folder_entity["path"].split("/") + hierarchy_items.pop(0) return { "project_name": project_name, - "asset_id": str(asset_doc["_id"]), - "asset_name": asset_name, + "folder_id": folder_id, + "folder_path": folder_entity["path"], "task_name": task_name, "task_type": task_type, - "hierarchy": hierarchy_items + "asset_id": folder_id, + "asset_name": folder_entity["name"], + "hierarchy": hierarchy_items, } - def start_timer(self, project_name, asset_name, task_name): + def start_timer(self, project_name, folder_path, task_name): """Start timer for passed context. Args: - project_name (str): Project name - asset_name (str): Asset name - task_name (str): Task name + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. """ data = self.get_timer_data_for_context( - project_name, asset_name, task_name, self.log + project_name, folder_path, task_name, self.log ) self.timer_started(None, data) - def get_task_time(self, project_name, asset_name, task_name): + def get_task_time(self, project_name, folder_path, task_name): """Get total time for passed context. TODO: @@ -281,7 +283,7 @@ class TimersManager( if hasattr(connector, "get_task_time"): module = self._modules_by_id[module_id] times[module.name] = connector.get_task_time( - project_name, asset_name, task_name + project_name, folder_path, task_name ) return times @@ -394,7 +396,7 @@ class TimersManager( @staticmethod def start_timer_with_webserver( - project_name, asset_name, task_name, logger=None + project_name, folder_path, task_name, logger=None ): """Prepared method for calling change timers on REST api. @@ -403,7 +405,7 @@ class TimersManager( Args: project_name (str): Project name. - asset_name (str): Asset name. + folder_path (str): Folder path. task_name (str): Task name. logger (logging.Logger): Logger object. Using 'print' if not passed. @@ -430,7 +432,7 @@ class TimersManager( return data = { "project_name": project_name, - "folder_path": asset_name, + "folder_path": folder_path, "task_name": task_name } @@ -472,13 +474,13 @@ class TimersManager( def _on_host_task_change(self, event): project_name = event["project_name"] - asset_name = event["folder_path"] + folder_path = event["folder_path"] task_name = event["task_name"] self.log.debug(( "Sending message that timer should change to" - " Project: {} Asset: {} Task: {}" - ).format(project_name, asset_name, task_name)) + " Project: {} Folder: {} Task: {}" + ).format(project_name, folder_path, task_name)) self.start_timer_with_webserver( - project_name, asset_name, task_name, self.log + project_name, folder_path, task_name, self.log ) diff --git a/client/ayon_core/pipeline/__init__.py b/client/ayon_core/pipeline/__init__.py index 679e9a195e..d1a181a353 100644 --- a/client/ayon_core/pipeline/__init__.py +++ b/client/ayon_core/pipeline/__init__.py @@ -31,7 +31,7 @@ from .load import ( HeroVersionType, IncompatibleLoaderError, LoaderPlugin, - SubsetLoaderPlugin, + ProductLoaderPlugin, discover_loader_plugins, register_loader_plugin, @@ -94,7 +94,7 @@ from .context_tools import ( get_current_context, get_current_host_name, get_current_project_name, - get_current_asset_name, + get_current_folder_path, get_current_task_name ) install = install_host @@ -136,7 +136,7 @@ __all__ = ( "HeroVersionType", "IncompatibleLoaderError", "LoaderPlugin", - "SubsetLoaderPlugin", + "ProductLoaderPlugin", "discover_loader_plugins", "register_loader_plugin", @@ -195,7 +195,7 @@ __all__ = ( "get_current_context", "get_current_host_name", "get_current_project_name", - "get_current_asset_name", + "get_current_folder_path", "get_current_task_name", # Backwards compatible function names diff --git a/client/ayon_core/pipeline/anatomy.py b/client/ayon_core/pipeline/anatomy.py index e7833a9a15..d6e09bad39 100644 --- a/client/ayon_core/pipeline/anatomy.py +++ b/client/ayon_core/pipeline/anatomy.py @@ -4,11 +4,11 @@ import copy import platform import collections import numbers - -import six import time -from ayon_core.client import get_project, get_ayon_server_api_connection +import six +import ayon_api + from ayon_core.lib import Logger, get_local_site_id from ayon_core.lib.path_templates import ( TemplateUnsolved, @@ -50,13 +50,13 @@ class BaseAnatomy(object): root_key_regex = re.compile(r"{(root?[^}]+)}") root_name_regex = re.compile(r"root\[([^]]+)\]") - def __init__(self, project_doc, root_overrides=None): - project_name = project_doc["name"] + def __init__(self, project_entity, root_overrides=None): + project_name = project_entity["name"] self.project_name = project_name - self.project_code = project_doc["data"]["code"] + self.project_code = project_entity["code"] self._data = self._prepare_anatomy_data( - project_doc, root_overrides + project_entity, root_overrides ) self._templates_obj = AnatomyTemplates(self) self._roots_obj = Roots(self) @@ -78,13 +78,13 @@ class BaseAnatomy(object): def items(self): return copy.deepcopy(self._data).items() - def _prepare_anatomy_data(self, project_doc, root_overrides): + def _prepare_anatomy_data(self, project_entity, root_overrides): """Prepare anatomy data for further processing. Method added to replace `{task}` with `{task[name]}` in templates. """ - anatomy_data = self._project_doc_to_anatomy_data(project_doc) + anatomy_data = self._project_entity_to_anatomy_data(project_entity) self._apply_local_settings_on_anatomy_data( anatomy_data, @@ -280,13 +280,13 @@ class BaseAnatomy(object): ``` ## Entered filepath - "P:/projects/project/asset/task/animation_v001.ma" + "P:/projects/project/folder/task/animation_v001.ma" ## Entered template "<{}>" ## Output - "/project/asset/task/animation_v001.ma" + "/project/folder/task/animation_v001.ma" Args: filepath (str): Full file path where root should be replaced. @@ -311,14 +311,21 @@ class BaseAnatomy(object): data = self.root_environmets_fill_data(template) return rootless_path.format(**data) - def _project_doc_to_anatomy_data(self, project_doc): + def _project_entity_to_anatomy_data(self, project_entity): """Convert project document to anatomy data. Probably should fill missing keys and values. """ - output = copy.deepcopy(project_doc["config"]) - output["attributes"] = copy.deepcopy(project_doc["data"]) + output = copy.deepcopy(project_entity["config"]) + # TODO remove AYON convertion + task_types = copy.deepcopy(project_entity["taskTypes"]) + new_task_types = {} + for task_type in task_types: + name = task_type["name"] + new_task_types[name] = task_type + output["tasks"] = new_task_types + output["attributes"] = copy.deepcopy(project_entity["attrib"]) return output @@ -418,7 +425,9 @@ class Anatomy(BaseAnatomy): lambda: collections.defaultdict(CacheItem) ) - def __init__(self, project_name=None, site_name=None): + def __init__( + self, project_name=None, site_name=None, project_entity=None + ): if not project_name: project_name = os.environ.get("AYON_PROJECT_NAME") @@ -428,16 +437,19 @@ class Anatomy(BaseAnatomy): " to load data for specific project." )) - project_doc = self.get_project_doc_from_cache(project_name) - root_overrides = self._get_site_root_overrides(project_name, site_name) + if not project_entity: + project_entity = self.get_project_entity_from_cache(project_name) + root_overrides = self._get_site_root_overrides( + project_name, site_name + ) - super(Anatomy, self).__init__(project_doc, root_overrides) + super(Anatomy, self).__init__(project_entity, root_overrides) @classmethod - def get_project_doc_from_cache(cls, project_name): + def get_project_entity_from_cache(cls, project_name): project_cache = cls._project_cache[project_name] if project_cache.is_outdated: - project_cache.update_data(get_project(project_name)) + project_cache.update_data(ayon_api.get_project(project_name)) return copy.deepcopy(project_cache.data) @classmethod @@ -468,8 +480,7 @@ class Anatomy(BaseAnatomy): """ if not project_name: return - con = get_ayon_server_api_connection() - return con.get_project_roots_for_site( + return ayon_api.get_project_roots_for_site( project_name, get_local_site_id() ) @@ -642,13 +653,76 @@ class AnatomyTemplates(TemplatesDict): return self._solve_dict(value, data) return super(AnatomyTemplates, self)._format_value(value, data) + @staticmethod + def _ayon_template_conversion(templates): + def _convert_template_item(template_item): + # Change 'directory' to 'folder' + if "directory" in template_item: + template_item["folder"] = template_item["directory"] + + if ( + "path" not in template_item + and "file" in template_item + and "folder" in template_item + ): + template_item["path"] = "/".join( + (template_item["folder"], template_item["file"]) + ) + + def _get_default_template_name(templates): + default_template = None + for name, template in templates.items(): + if name == "default": + return "default" + + if default_template is None: + default_template = name + + return default_template + + def _fill_template_category(templates, cat_templates, cat_key): + default_template_name = _get_default_template_name(cat_templates) + for template_name, cat_template in cat_templates.items(): + _convert_template_item(cat_template) + if template_name == default_template_name: + templates[cat_key] = cat_template + else: + new_name = "{}_{}".format(cat_key, template_name) + templates["others"][new_name] = cat_template + + others_templates = templates.pop("others", None) or {} + new_others_templates = {} + templates["others"] = new_others_templates + for name, template in others_templates.items(): + _convert_template_item(template) + new_others_templates[name] = template + + for key in ( + "work", + "publish", + "hero", + ): + cat_templates = templates.pop(key) + _fill_template_category(templates, cat_templates, key) + + delivery_templates = templates.pop("delivery", None) or {} + new_delivery_templates = {} + for name, delivery_template in delivery_templates.items(): + new_delivery_templates[name] = "/".join( + (delivery_template["directory"], delivery_template["file"]) + ) + templates["delivery"] = new_delivery_templates + def set_templates(self, templates): if not templates: self.reset() return - self._raw_templates = copy.deepcopy(templates) templates = copy.deepcopy(templates) + # TODO remove AYON convertion + self._ayon_template_conversion(templates) + + self._raw_templates = copy.deepcopy(templates) v_queue = collections.deque() v_queue.append(templates) while v_queue: @@ -834,7 +908,7 @@ class AnatomyTemplates(TemplatesDict): key_2: "value_2" key_4: "value_3/value_2" """ - default_key_values = templates.pop("defaults", {}) + default_key_values = templates.pop("common", {}) for key, value in tuple(templates.items()): if isinstance(value, dict): continue diff --git a/client/ayon_core/pipeline/colorspace.py b/client/ayon_core/pipeline/colorspace.py index 7100984217..034c90d27b 100644 --- a/client/ayon_core/pipeline/colorspace.py +++ b/client/ayon_core/pipeline/colorspace.py @@ -774,8 +774,8 @@ def get_imageio_config( if not anatomy_data: from ayon_core.pipeline.context_tools import ( - get_template_data_from_session) - anatomy_data = get_template_data_from_session() + get_current_context_template_data) + anatomy_data = get_current_context_template_data() formatting_data = deepcopy(anatomy_data) diff --git a/client/ayon_core/pipeline/context_tools.py b/client/ayon_core/pipeline/context_tools.py index 86b3d770b4..50c384bf88 100644 --- a/client/ayon_core/pipeline/context_tools.py +++ b/client/ayon_core/pipeline/context_tools.py @@ -6,21 +6,13 @@ import logging import platform import uuid +import ayon_api import pyblish.api from pyblish.lib import MessageHandler from ayon_core import AYON_CORE_ROOT from ayon_core.host import HostBase -from ayon_core.client import ( - get_project, - get_asset_by_id, - get_asset_by_name, - version_is_latest, - get_asset_name_identifier, - get_ayon_server_api_connection, -) -from ayon_core.lib import is_in_tests -from ayon_core.lib.events import emit_event +from ayon_core.lib import is_in_tests, initialize_ayon_connection, emit_event from ayon_core.addon import load_addons, AddonsManager from ayon_core.settings import get_project_settings @@ -113,7 +105,7 @@ def install_host(host): _is_installed = True # Make sure global AYON connection has set site id and version - get_ayon_server_api_connection() + initialize_ayon_connection() addons_manager = _get_addons_manager() @@ -162,7 +154,10 @@ def install_host(host): def install_ayon_plugins(project_name=None, host_name=None): - # Make sure modules are loaded + # Make sure global AYON connection has set site id and version + # - this is necessary if 'install_host' is not called + initialize_ayon_connection() + # Make sure addons are loaded load_addons() log.info("Registering global plug-ins..") @@ -385,10 +380,10 @@ def get_current_project_name(): return get_global_context()["project_name"] -def get_current_asset_name(): +def get_current_folder_path(): host = registered_host() if isinstance(host, HostBase): - return host.get_current_asset_name() + return host.get_current_folder_path() return get_global_context()["folder_path"] @@ -399,51 +394,57 @@ def get_current_task_name(): return get_global_context()["task_name"] -def get_current_project(fields=None): +def get_current_project_entity(fields=None): """Helper function to get project document based on global Session. This function should be called only in process where host is installed. + Args: + fields (Optional[Iterable[str]]): Limit returned data of project + entity. + Returns: - dict: Project document. - None: Project is not set. + Union[dict[str, Any], None]: Project entity of current project or None. + """ - project_name = get_current_project_name() - return get_project(project_name, fields=fields) + return ayon_api.get_project(project_name, fields=fields) -def get_current_project_asset(asset_name=None, asset_id=None, fields=None): - """Helper function to get asset document based on global Session. +def get_current_project_folder(folder_path=None, folder_id=None, fields=None): + """Helper function to get folder entity based on current context. This function should be called only in process where host is installed. - Asset is found out based on passed asset name or id (not both). Asset name - is not used for filtering if asset id is passed. When both asset name and - id are missing then asset name from current process is used. + Folder is found out based on passed folder path or id (not both). Folder + path is not used for filtering if folder id is passed. When both + folder path and id are missing then current folder path is used. Args: - asset_name (str): Name of asset used for filter. - asset_id (Union[str, ObjectId]): Asset document id. If entered then + folder_path (Union[str, None]): Folder path used for filter. + folder_id (Union[str, None]): Folder id. If entered then is used as only filter. - fields (Union[List[str], None]): Limit returned data of asset documents + fields (Optional[Iterable[str]]): Limit returned data of folder entity to specific keys. Returns: - dict: Asset document. - None: Asset is not set or not exist. + Union[dict[str, Any], None]: Fodler entity or None. """ project_name = get_current_project_name() - if asset_id: - return get_asset_by_id(project_name, asset_id, fields=fields) + if folder_id: + return ayon_api.get_folder_by_id( + project_name, folder_id, fields=fields + ) - if not asset_name: - asset_name = get_current_asset_name() + if not folder_path: + folder_path = get_current_folder_path() # Skip if is not set even on context - if not asset_name: + if not folder_path: return None - return get_asset_by_name(project_name, asset_name, fields=fields) + return ayon_api.get_folder_by_path( + project_name, folder_path, fields=fields + ) def is_representation_from_latest(representation): @@ -457,7 +458,9 @@ def is_representation_from_latest(representation): """ project_name = get_current_project_name() - return version_is_latest(project_name, representation["parent"]) + return ayon_api.version_is_latest( + project_name, representation["versionId"] + ) def get_template_data_from_session(session=None, settings=None): @@ -475,18 +478,18 @@ def get_template_data_from_session(session=None, settings=None): if session is not None: project_name = session["AYON_PROJECT_NAME"] - asset_name = session["AYON_FOLDER_PATH"] + folder_path = session["AYON_FOLDER_PATH"] task_name = session["AYON_TASK_NAME"] host_name = session["AYON_HOST_NAME"] else: context = get_current_context() project_name = context["project_name"] - asset_name = context["folder_path"] + folder_path = context["folder_path"] task_name = context["task_name"] host_name = get_current_host_name() return get_template_data_with_names( - project_name, asset_name, task_name, host_name, settings + project_name, folder_path, task_name, host_name, settings ) @@ -503,12 +506,12 @@ def get_current_context_template_data(settings=None): context = get_current_context() project_name = context["project_name"] - asset_name = context["folder_path"] + folder_path = context["folder_path"] task_name = context["task_name"] host_name = get_current_host_name() return get_template_data_with_names( - project_name, asset_name, task_name, host_name, settings + project_name, folder_path, task_name, host_name, settings ) @@ -536,9 +539,9 @@ def get_workdir_from_session(session=None, template_key=None): if not template_key: task_type = template_data["task"]["type"] template_key = get_workfile_template_key( + project_name, task_type, host_name, - project_name=project_name ) anatomy = Anatomy(project_name) @@ -568,33 +571,56 @@ def get_custom_workfile_template_from_session( if session is not None: project_name = session["AYON_PROJECT_NAME"] - asset_name = session["AYON_FOLDER_PATH"] + folder_path = session["AYON_FOLDER_PATH"] task_name = session["AYON_TASK_NAME"] host_name = session["AYON_HOST_NAME"] else: context = get_current_context() project_name = context["project_name"] - asset_name = context["folder_path"] + folder_path = context["folder_path"] task_name = context["task_name"] host_name = get_current_host_name() return get_custom_workfile_template_by_string_context( project_name, - asset_name, + folder_path, task_name, host_name, project_settings=project_settings ) -def change_current_context(asset_doc, task_name, template_key=None): - """Update active Session to a new task work area. +def get_current_context_custom_workfile_template(project_settings=None): + """Filter and fill workfile template profiles by current context. - This updates the live Session to a different task under asset. + This function can be used only inside host where context is set. Args: - asset_doc (Dict[str, Any]): The asset document to set. - task_name (str): The task to set under asset. + project_settings(Optional[Dict[str, Any]]): Project settings. + + Returns: + str: Path to template or None if none of profiles match current + context. (Existence of formatted path is not validated.) + + """ + context = get_current_context() + return get_custom_workfile_template_by_string_context( + context["project_name"], + context["folder_path"], + context["task_name"], + get_current_host_name(), + project_settings=project_settings + ) + + +def change_current_context(folder_entity, task_entity, template_key=None): + """Update active Session to a new task work area. + + This updates the live Session to a different task under folder. + + Args: + folder_entity (Dict[str, Any]): Folder entity to set. + task_entity (Dict[str, Any]): Task entity to set. template_key (Union[str, None]): Prepared template key to be used for workfile template in Anatomy. @@ -604,18 +630,22 @@ def change_current_context(asset_doc, task_name, template_key=None): project_name = get_current_project_name() workdir = None - if asset_doc: - project_doc = get_project(project_name) + folder_path = None + task_name = None + if folder_entity: + folder_path = folder_entity["path"] + if task_entity: + task_name = task_entity["name"] + project_entity = ayon_api.get_project(project_name) host_name = get_current_host_name() workdir = get_workdir( - project_doc, - asset_doc, - task_name, + project_entity, + folder_entity, + task_entity, host_name, template_key=template_key ) - folder_path = get_asset_name_identifier(asset_doc) envs = { "AYON_PROJECT_NAME": project_name, "AYON_FOLDER_PATH": folder_path, @@ -635,7 +665,7 @@ def change_current_context(asset_doc, task_name, template_key=None): # Convert env keys to human readable keys data["project_name"] = project_name - data["folder_path"] = get_asset_name_identifier(asset_doc) + data["folder_path"] = folder_path data["task_name"] = task_name data["workdir_path"] = workdir diff --git a/client/ayon_core/pipeline/create/context.py b/client/ayon_core/pipeline/create/context.py index 425de4305f..308762fae6 100644 --- a/client/ayon_core/pipeline/create/context.py +++ b/client/ayon_core/pipeline/create/context.py @@ -10,12 +10,8 @@ from contextlib import contextmanager import pyblish.logic import pyblish.api +import ayon_api -from ayon_core.client import ( - get_assets, - get_asset_by_name, - get_asset_name_identifier, -) from ayon_core.settings import get_project_settings from ayon_core.lib.attribute_definitions import ( UnknownDef, @@ -854,7 +850,7 @@ class CreatedInstance: """Instance entity with data that will be stored to workfile. I think `data` must be required argument containing all minimum information - about instance like "asset" and "task" and all data used for filling + about instance like "folderPath" and "task" and all data used for filling product name as creators may have custom data for product name filling. Notes: @@ -982,7 +978,7 @@ class CreatedInstance: if not self._data.get("instance_id"): self._data["instance_id"] = str(uuid4()) - self._asset_is_valid = self.has_set_asset + self._folder_is_valid = self.has_set_folder self._task_is_valid = self.has_set_task def __str__(self): @@ -1283,8 +1279,8 @@ class CreatedInstance: # Context validation related methods/properties @property - def has_set_asset(self): - """Asset name is set in data.""" + def has_set_folder(self): + """Folder path is set in data.""" return "folderPath" in self._data @@ -1298,15 +1294,15 @@ class CreatedInstance: def has_valid_context(self): """Context data are valid for publishing.""" - return self.has_valid_asset and self.has_valid_task + return self.has_valid_folder and self.has_valid_task @property - def has_valid_asset(self): - """Asset set in context exists in project.""" + def has_valid_folder(self): + """Folder set in context exists in project.""" - if not self.has_set_asset: + if not self.has_set_folder: return False - return self._asset_is_valid + return self._folder_is_valid @property def has_valid_task(self): @@ -1316,9 +1312,9 @@ class CreatedInstance: return False return self._task_is_valid - def set_asset_invalid(self, invalid): - # TODO replace with `set_asset_name` - self._asset_is_valid = not invalid + def set_folder_invalid(self, invalid): + # TODO replace with `set_folder_path` + self._folder_is_valid = not invalid def set_task_invalid(self, invalid): # TODO replace with `set_task_name` @@ -1402,7 +1398,7 @@ class CreateContext: ).format(joined_methods)) self._current_project_name = None - self._current_asset_name = None + self._current_folder_path = None self._current_task_name = None self._current_workfile_path = None @@ -1557,14 +1553,14 @@ class CreateContext: return self._current_project_name - def get_current_asset_name(self): - """Asset name which was used as current context on context reset. + def get_current_folder_path(self): + """Folder path which was used as current context on context reset. Returns: - Union[str, None]: Asset name. + Union[str, None]: Folder path. """ - return self._current_asset_name + return self._current_folder_path def get_current_task_name(self): """Task name which was used as current context on context reset. @@ -1600,19 +1596,19 @@ class CreateContext: def context_has_changed(self): """Host context has changed. - As context is used project, asset, task name and workfile path if + As context is used project, folder, task name and workfile path if host does support workfiles. Returns: bool: Context changed. """ - project_name, asset_name, task_name, workfile_path = ( + project_name, folder_path, task_name, workfile_path = ( self._get_current_host_context() ) return ( self._current_project_name != project_name - or self._current_asset_name != asset_name + or self._current_folder_path != folder_path or self._current_task_name != task_name or self._current_workfile_path != workfile_path ) @@ -1683,18 +1679,18 @@ class CreateContext: self.refresh_thumbnails() def _get_current_host_context(self): - project_name = asset_name = task_name = workfile_path = None + project_name = folder_path = task_name = workfile_path = None if hasattr(self.host, "get_current_context"): host_context = self.host.get_current_context() if host_context: project_name = host_context.get("project_name") - asset_name = host_context.get("folder_path") + folder_path = host_context.get("folder_path") task_name = host_context.get("task_name") if isinstance(self.host, IWorkfileHost): workfile_path = self.host.get_current_workfile() - return project_name, asset_name, task_name, workfile_path + return project_name, folder_path, task_name, workfile_path def reset_current_context(self): """Refresh current context. @@ -1713,12 +1709,12 @@ class CreateContext: are stored. We should store the workfile (if is available) too. """ - project_name, asset_name, task_name, workfile_path = ( + project_name, folder_path, task_name, workfile_path = ( self._get_current_host_context() ) self._current_project_name = project_name - self._current_asset_name = asset_name + self._current_folder_path = folder_path self._current_task_name = task_name self._current_workfile_path = workfile_path @@ -1950,44 +1946,51 @@ class CreateContext: self, creator_identifier, variant, - asset_doc=None, - task_name=None, + folder_entity=None, + task_entity=None, pre_create_data=None ): """Trigger create of plugins with standartized arguments. - Arguments 'asset_doc' and 'task_name' use current context as default - values. If only 'task_name' is provided it will be overriden by - task name from current context. If 'task_name' is not provided - when 'asset_doc' is, it is considered that task name is not specified, - which can lead to error if product name template requires task name. + Arguments 'folder_entity' and 'task_name' use current context as + default values. If only 'task_name' is provided it will be overriden + by task name from current context. If 'task_name' is not provided + when 'folder_entity' is, it is considered that task name is not + specified, which can lead to error if product name template requires + task name. Args: creator_identifier (str): Identifier of creator plugin. variant (str): Variant used for product name. - asset_doc (Dict[str, Any]): Asset document which define context of - creation (possible context of created instance/s). - task_name (str): Name of task to which is context related. + folder_entity (Dict[str, Any]): Folder entity which define context + of creation (possible context of created instance/s). + task_entity (Dict[str, Any]): Task entity. pre_create_data (Dict[str, Any]): Pre-create attribute values. Returns: Any: Output of triggered creator's 'create' method. Raises: - CreatorError: If creator was not found or asset is empty. + CreatorError: If creator was not found or folder is empty. """ creator = self._get_creator_in_create(creator_identifier) project_name = self.project_name - if asset_doc is None: - asset_name = self.get_current_asset_name() - asset_doc = get_asset_by_name(project_name, asset_name) - task_name = self.get_current_task_name() - if asset_doc is None: + if folder_entity is None: + folder_path = self.get_current_folder_path() + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path + ) + if folder_entity is None: raise CreatorError( - "Asset with name {} was not found".format(asset_name) + "Folder '{}' was not found".format(folder_path) ) + if task_entity is None: + task_name = self.get_current_task_name() + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) if pre_create_data is None: pre_create_data = {} @@ -2005,15 +2008,14 @@ class CreateContext: product_name = creator.get_product_name( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, self.host_name, ) - asset_name = get_asset_name_identifier(asset_doc) instance_data = { - "folderPath": asset_name, + "folderPath": folder_entity["path"], "task": task_name, "productType": creator.product_type, "variant": variant @@ -2226,7 +2228,7 @@ class CreateContext: raise CreatorsCreateFailed(failed_info) def validate_instances_context(self, instances=None): - """Validate 'asset' and 'task' instance context.""" + """Validate 'folder' and 'task' instance context.""" # Use all instances from context if 'instances' are not passed if instances is None: instances = tuple(self._instances_by_id.values()) @@ -2235,54 +2237,83 @@ class CreateContext: if not instances: return - task_names_by_asset_name = {} + project_name = self.project_name + + task_names_by_folder_path = {} for instance in instances: - asset_name = instance.get("folderPath") + folder_path = instance.get("folderPath") task_name = instance.get("task") - if asset_name: - task_names_by_asset_name[asset_name] = set() + if folder_path: + task_names_by_folder_path[folder_path] = set() if task_name: - task_names_by_asset_name[asset_name].add(task_name) + task_names_by_folder_path[folder_path].add(task_name) - asset_names = { - asset_name - for asset_name in task_names_by_asset_name.keys() - if asset_name is not None - } - asset_docs = list(get_assets( - self.project_name, - asset_names=asset_names, - fields={"name", "data.tasks", "data.parents"} - )) + # Backwards compatibility for cases where folder name is set instead + # of folder path + folder_names = set() + folder_paths = set() + for folder_path in task_names_by_folder_path.keys(): + if folder_path is None: + pass + elif "/" in folder_path: + folder_paths.add(folder_path) + else: + folder_names.add(folder_path) - task_names_by_asset_name = {} - asset_docs_by_name = collections.defaultdict(list) - for asset_doc in asset_docs: - asset_name = get_asset_name_identifier(asset_doc) - tasks = asset_doc.get("data", {}).get("tasks") or {} - task_names_by_asset_name[asset_name] = set(tasks.keys()) - asset_docs_by_name[asset_doc["name"]].append(asset_doc) + folder_paths_by_id = {} + if folder_paths: + for folder_entity in ayon_api.get_folders( + project_name, + folder_paths=folder_paths, + fields={"id", "path"} + ): + folder_id = folder_entity["id"] + folder_paths_by_id[folder_id] = folder_entity["path"] + + folder_entities_by_name = collections.defaultdict(list) + if folder_names: + for folder_entity in ayon_api.get_folders( + project_name, + folder_names=folder_names, + fields={"id", "name", "path"} + ): + folder_id = folder_entity["id"] + folder_name = folder_entity["name"] + folder_paths_by_id[folder_id] = folder_entity["path"] + folder_entities_by_name[folder_name].append(folder_entity) + + tasks_entities = ayon_api.get_tasks( + project_name, + folder_ids=folder_paths_by_id.keys(), + fields={"name", "folderId"} + ) + + task_names_by_folder_path = collections.defaultdict(set) + for task_entity in tasks_entities: + folder_id = task_entity["folderId"] + folder_path = folder_paths_by_id[folder_id] + task_names_by_folder_path[folder_path].add(task_entity["name"]) for instance in instances: - if not instance.has_valid_asset or not instance.has_valid_task: + if not instance.has_valid_folder or not instance.has_valid_task: continue - asset_name = instance["folderPath"] - if asset_name and "/" not in asset_name: - asset_docs = asset_docs_by_name.get(asset_name) - if len(asset_docs) == 1: - asset_name = get_asset_name_identifier(asset_docs[0]) - instance["folderPath"] = asset_name + folder_path = instance["folderPath"] + if folder_path and "/" not in folder_path: + folder_entities = folder_entities_by_name.get(folder_path) + if len(folder_entities) == 1: + folder_path = folder_entities[0]["path"] + instance["folderPath"] = folder_path - if asset_name not in task_names_by_asset_name: - instance.set_asset_invalid(True) + if folder_path not in task_names_by_folder_path: + instance.set_folder_invalid(True) continue task_name = instance["task"] if not task_name: continue - if task_name not in task_names_by_asset_name[asset_name]: + if task_name not in task_names_by_folder_path[folder_path]: instance.set_task_invalid(True) def save_changes(self): diff --git a/client/ayon_core/pipeline/create/creator_plugins.py b/client/ayon_core/pipeline/create/creator_plugins.py index cb8e4a2d1c..5505427d7e 100644 --- a/client/ayon_core/pipeline/create/creator_plugins.py +++ b/client/ayon_core/pipeline/create/creator_plugins.py @@ -33,7 +33,7 @@ class CreatorError(Exception): @six.add_metaclass(ABCMeta) -class SubsetConvertorPlugin(object): +class ProductConvertorPlugin(object): """Helper for conversion of instances created using legacy creators. Conversion from legacy creators would mean to loose legacy instances, @@ -472,8 +472,8 @@ class BaseCreator: def get_dynamic_data( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, instance @@ -489,31 +489,21 @@ class BaseCreator: def get_product_name( self, project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name=None, instance=None ): """Return product name for passed context. - CHANGES: - Argument `asset_id` was replaced with `asset_doc`. It is easier to - query asset before. In some cases would this method be called multiple - times and it would be too slow to query asset document on each - callback. - - NOTE: - Asset document is not used yet but is required if would like to use - task type in product templates. - Method is also called on product name update. In that case origin instance is passed in. Args: project_name (str): Project name. - asset_doc (dict): Asset document for which product is created. - task_name (str): For which task product is created. + folder_entity (dict): Folder entity. + task_entity (dict): Task entity. variant (str): Product name variant. In most of cases user input. host_name (Optional[str]): Which host creates product. Defaults to host name on create context. @@ -524,10 +514,16 @@ class BaseCreator: if host_name is None: host_name = self.create_context.host_name + + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] + dynamic_data = self.get_dynamic_data( project_name, - asset_doc, - task_name, + folder_entity, + task_entity, variant, host_name, instance @@ -535,8 +531,8 @@ class BaseCreator: return get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, self.product_type, variant, @@ -815,7 +811,7 @@ def discover_creator_plugins(*args, **kwargs): def discover_convertor_plugins(*args, **kwargs): - return discover(SubsetConvertorPlugin, *args, **kwargs) + return discover(ProductConvertorPlugin, *args, **kwargs) def discover_legacy_creator_plugins(): @@ -874,8 +870,8 @@ def register_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): register_plugin(LegacyCreator, plugin) - elif issubclass(plugin, SubsetConvertorPlugin): - register_plugin(SubsetConvertorPlugin, plugin) + elif issubclass(plugin, ProductConvertorPlugin): + register_plugin(ProductConvertorPlugin, plugin) def deregister_creator_plugin(plugin): @@ -885,20 +881,20 @@ def deregister_creator_plugin(plugin): elif issubclass(plugin, LegacyCreator): deregister_plugin(LegacyCreator, plugin) - elif issubclass(plugin, SubsetConvertorPlugin): - deregister_plugin(SubsetConvertorPlugin, plugin) + elif issubclass(plugin, ProductConvertorPlugin): + deregister_plugin(ProductConvertorPlugin, plugin) def register_creator_plugin_path(path): register_plugin_path(BaseCreator, path) register_plugin_path(LegacyCreator, path) - register_plugin_path(SubsetConvertorPlugin, path) + register_plugin_path(ProductConvertorPlugin, path) def deregister_creator_plugin_path(path): deregister_plugin_path(BaseCreator, path) deregister_plugin_path(LegacyCreator, path) - deregister_plugin_path(SubsetConvertorPlugin, path) + deregister_plugin_path(ProductConvertorPlugin, path) def cache_and_get_instances(creator, shared_key, list_instances_func): diff --git a/client/ayon_core/pipeline/create/legacy_create.py b/client/ayon_core/pipeline/create/legacy_create.py index 5e23a74a79..a8a39b41e3 100644 --- a/client/ayon_core/pipeline/create/legacy_create.py +++ b/client/ayon_core/pipeline/create/legacy_create.py @@ -9,7 +9,6 @@ import os import logging import collections -from ayon_core.client import get_asset_by_id from ayon_core.pipeline.constants import AVALON_INSTANCE_ID from .product_name import get_product_name @@ -89,7 +88,7 @@ class LegacyCreator(object): @classmethod def get_dynamic_data( - cls, project_name, asset_id, task_name, variant, host_name + cls, project_name, folder_entity, task_entity, variant, host_name ): """Return dynamic data for current Creator plugin. @@ -124,7 +123,7 @@ class LegacyCreator(object): @classmethod def get_product_name( - cls, project_name, asset_id, task_name, variant, host_name=None + cls, project_name, folder_entity, task_entity, variant, host_name=None ): """Return product name created with entered arguments. @@ -137,8 +136,8 @@ class LegacyCreator(object): Args: project_name (str): Context's project name. - asset_id (str): Folder id. - task_name (str): Context's task name. + folder_entity (dict[str, Any]): Folder entity. + task_entity (dict[str, Any]): Task entity. variant (str): What is entered by user in creator tool. host_name (str): Name of host. @@ -148,17 +147,16 @@ class LegacyCreator(object): """ dynamic_data = cls.get_dynamic_data( - project_name, asset_id, task_name, variant, host_name + project_name, folder_entity, task_entity, variant, host_name ) - - asset_doc = get_asset_by_id( - project_name, asset_id, fields=["data.tasks"] - ) - + task_name = task_type = None + if task_entity: + task_name = task_entity["name"] + task_type = task_entity["taskType"] return get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, cls.product_type, variant, @@ -166,7 +164,9 @@ class LegacyCreator(object): ) -def legacy_create(Creator, name, asset, options=None, data=None): +def legacy_create( + Creator, product_name, folder_path, options=None, data=None +): """Create a new instance Associate nodes with a product name and type. These nodes are later @@ -178,11 +178,11 @@ def legacy_create(Creator, name, asset, options=None, data=None): and finally asset browsers to help identify the origin of the asset. Arguments: - Creator (Creator): Class of creator - name (str): Name of product - asset (str): Name of asset - options (dict, optional): Additional options from GUI - data (dict, optional): Additional data from GUI + Creator (Creator): Class of creator. + product_name (str): Name of product. + folder_path (str): Folder path. + options (dict, optional): Additional options from GUI. + data (dict, optional): Additional data from GUI. Raises: NameError on `productName` already exists @@ -196,7 +196,7 @@ def legacy_create(Creator, name, asset, options=None, data=None): from ayon_core.pipeline import registered_host host = registered_host() - plugin = Creator(name, asset, options, data) + plugin = Creator(product_name, folder_path, options, data) if plugin.maintain_selection is True: with host.maintained_selection(): diff --git a/client/ayon_core/pipeline/create/product_name.py b/client/ayon_core/pipeline/create/product_name.py index 8413bfa9d8..74e268fbb3 100644 --- a/client/ayon_core/pipeline/create/product_name.py +++ b/client/ayon_core/pipeline/create/product_name.py @@ -1,4 +1,4 @@ -import os +import ayon_api from ayon_core.settings import get_project_settings from ayon_core.lib import filter_profiles, prepare_template_data @@ -33,7 +33,7 @@ def get_product_name_template( Args: project_name (str): Project on which the context lives. - product_type (str): Product type for which the subset name is + product_type (str): Product type for which the product name is calculated. host_name (str): Name of host in which the product name is calculated. task_name (str): Name of task in which context the product is created. @@ -81,8 +81,8 @@ def get_product_name_template( def get_product_name( project_name, - asset_doc, task_name, + task_type, host_name, product_type, variant, @@ -107,12 +107,11 @@ def get_product_name( Args: project_name (str): Project name. + task_name (Union[str, None]): Task name. + task_type (Union[str, None]): Task type. host_name (str): Host name. product_type (str): Product type. variant (str): In most of the cases it is user input during creation. - task_name (str): Task name on which context is instance created. - asset_doc (dict): Queried asset document with its tasks in data. - Used to get task type. default_template (Optional[str]): Default template if any profile does not match passed context. Constant 'DEFAULT_PRODUCT_TEMPLATE' is used if is not passed. @@ -132,10 +131,6 @@ def get_product_name( if not product_type: return "" - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") - template = get_product_name_template( project_name, product_type_filter or product_type, diff --git a/client/ayon_core/pipeline/create/utils.py b/client/ayon_core/pipeline/create/utils.py index 44063bd9ac..b43741e183 100644 --- a/client/ayon_core/pipeline/create/utils.py +++ b/client/ayon_core/pipeline/create/utils.py @@ -1,11 +1,6 @@ import collections -from ayon_core.client import ( - get_assets, - get_subsets, - get_last_versions, - get_asset_name_identifier, -) +import ayon_api def get_last_versions_for_instances( @@ -54,49 +49,50 @@ def get_last_versions_for_instances( if not product_names: return output - asset_docs = get_assets( + folder_entities = ayon_api.get_folders( project_name, - asset_names=product_names_by_folder_path.keys(), - fields=["name", "_id", "data.parents"] + folder_paths=product_names_by_folder_path.keys(), + fields={"id", "path"} ) folder_paths_by_id = { - asset_doc["_id"]: get_asset_name_identifier(asset_doc) - for asset_doc in asset_docs + folder_entity["id"]: folder_entity["path"] + for folder_entity in folder_entities } if not folder_paths_by_id: return output - subset_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=folder_paths_by_id.keys(), - subset_names=product_names, - fields=["_id", "name", "parent"] + folder_ids=folder_paths_by_id.keys(), + product_names=product_names, + fields={"id", "name", "folderId"} ) - subset_docs_by_id = {} - for subset_doc in subset_docs: - # Filter subset docs by subset names under parent - folder_id = subset_doc["parent"] + product_entities_by_id = {} + for product_entity in product_entities: + # Filter product entities by names under parent + folder_id = product_entity["folderId"] + product_name = product_entity["name"] folder_path = folder_paths_by_id[folder_id] - product_name = subset_doc["name"] if product_name not in product_names_by_folder_path[folder_path]: continue - subset_docs_by_id[subset_doc["_id"]] = subset_doc + product_entities_by_id[product_entity["id"]] = product_entity - if not subset_docs_by_id: + if not product_entities_by_id: return output - last_versions_by_product_id = get_last_versions( + last_versions_by_product_id = ayon_api.get_last_versions( project_name, - subset_docs_by_id.keys(), - fields=["name", "parent"] + product_entities_by_id.keys(), + fields={"version", "productId"} ) - for subset_id, version_doc in last_versions_by_product_id.items(): - subset_doc = subset_docs_by_id[subset_id] - folder_id = subset_doc["parent"] + for product_id, version_entity in last_versions_by_product_id.items(): + product_entity = product_entities_by_id[product_id] + product_name = product_entity["name"] + folder_id = product_entity["folderId"] folder_path = folder_paths_by_id[folder_id] - _instances = instances_by_hierarchy[folder_path][subset_doc["name"]] + _instances = instances_by_hierarchy[folder_path][product_name] for instance in _instances: - output[instance.id] = version_doc["name"] + output[instance.id] = version_entity["version"] return output diff --git a/client/ayon_core/pipeline/delivery.py b/client/ayon_core/pipeline/delivery.py index cb90e67090..d2b78422e3 100644 --- a/client/ayon_core/pipeline/delivery.py +++ b/client/ayon_core/pipeline/delivery.py @@ -143,7 +143,7 @@ def deliver_single_file( src_path = os.path.normpath(src_path.replace("\\", "/")) if not os.path.exists(src_path): - msg = "{} doesn't exist for {}".format(src_path, repre["_id"]) + msg = "{} doesn't exist for {}".format(src_path, repre["id"]) report_items["Source file was not found"].append(msg) return report_items, 0 @@ -216,7 +216,7 @@ def deliver_sequence( if not hash_path_exist(src_path): msg = "{} doesn't exist for {}".format( - src_path, repre["_id"]) + src_path, repre["id"]) report_items["Source file was not found"].append(msg) return report_items, 0 diff --git a/client/ayon_core/pipeline/farm/pyblish_functions.py b/client/ayon_core/pipeline/farm/pyblish_functions.py index c669d95c1e..dadf2cbe1a 100644 --- a/client/ayon_core/pipeline/farm/pyblish_functions.py +++ b/client/ayon_core/pipeline/farm/pyblish_functions.py @@ -1,21 +1,19 @@ -import copy -import attr -import pyblish.api import os -import clique -from copy import deepcopy +import copy import re import warnings +from copy import deepcopy + +import attr +import ayon_api +import pyblish.api +import clique from ayon_core.pipeline import ( get_current_project_name, get_representation_path, Anatomy, ) -from ayon_core.client import ( - get_last_version_by_subset_name, - get_representations -) from ayon_core.lib import Logger from ayon_core.pipeline.publish import KnownPublishError from ayon_core.pipeline.farm.patterning import match_aov_pattern @@ -78,16 +76,19 @@ def extend_frames(folder_path, product_name, start, end): prev_end = None project_name = get_current_project_name() - version = get_last_version_by_subset_name( + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} + ) + version_entity = ayon_api.get_last_version_by_product_name( project_name, product_name, - asset_name=folder_path + folder_entity["id"] ) # Set prev start / end frames for comparison if not prev_start and not prev_end: - prev_start = version["data"]["frameStart"] - prev_end = version["data"]["frameEnd"] + prev_start = version_entity["attrib"]["frameStart"] + prev_end = version_entity["attrib"]["frameEnd"] updated_start = min(start, prev_start) updated_end = max(end, prev_end) @@ -626,7 +627,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, new_instance = deepcopy(skeleton) new_instance["productName"] = product_name - new_instance["subsetGroup"] = group_name + new_instance["productGroup"] = group_name # explicitly disable review by user preview = preview and not do_not_add_review @@ -692,7 +693,7 @@ def _create_instances_for_aov(instance, skeleton, aov_filter, additional_data, return instances -def get_resources(project_name, version, extension=None): +def get_resources(project_name, version_entity, extension=None): """Get the files from the specific version. This will return all get all files from representation. @@ -710,7 +711,7 @@ def get_resources(project_name, version, extension=None): Args: project_name (str): Name of the project. - version (dict): Version document. + version_entity (dict): Version entity. extension (str): extension used to filter representations. @@ -727,13 +728,14 @@ def get_resources(project_name, version, extension=None): # there is a `context_filter` argument that won't probably work in # final release of AYON. SO we'll rather not use it - repre_docs = list(get_representations( - project_name, version_ids=[version["_id"]])) + repre_entities = list(ayon_api.get_representations( + project_name, version_ids={version_entity["id"]} + )) filtered = [] - for doc in repre_docs: - if doc["context"]["ext"] in extensions: - filtered.append(doc) + for repre_entity in repre_entities: + if repre_entity["context"]["ext"] in extensions: + filtered.append(repre_entity) representation = filtered[0] directory = get_representation_path(representation) @@ -1005,18 +1007,22 @@ def copy_extend_frames(instance, representation): project_name = instance.context.data["project"] anatomy = instance.context.data["anatomy"] # type: Anatomy + folder_entity = ayon_api.get_folder_by_path( + project_name, instance.data.get("folderPath") + ) + # get latest version of product # this will stop if product wasn't published yet - version = get_last_version_by_subset_name( + version_entity = ayon_api.get_last_version_by_product_name( project_name, instance.data.get("productName"), - asset_name=instance.data.get("folderPath") + folder_entity["id"] ) # get its files based on extension product_resources = get_resources( - project_name, version, representation.get("ext") + project_name, version_entity, representation.get("ext") ) r_col, _ = clique.assemble(product_resources) @@ -1088,8 +1094,8 @@ def attach_instances_to_product(attach_to, instances): new_inst["productType"] = attach_instance.get("productType") new_inst["family"] = attach_instance.get("family") new_inst["append"] = True - # don't set subsetGroup if we are attaching - new_inst.pop("subsetGroup") + # don't set productGroup if we are attaching + new_inst.pop("productGroup") new_instances.append(new_inst) return new_instances diff --git a/client/ayon_core/pipeline/load/__init__.py b/client/ayon_core/pipeline/load/__init__.py index ca11b26211..bdc5ece620 100644 --- a/client/ayon_core/pipeline/load/__init__.py +++ b/client/ayon_core/pipeline/load/__init__.py @@ -8,13 +8,14 @@ from .utils import ( LoaderNotFoundError, get_repres_contexts, - get_contexts_for_repre_docs, - get_subset_contexts, + get_product_contexts, get_representation_context, + get_representation_contexts, + get_representation_contexts_by_ids, load_with_repre_context, - load_with_subset_context, - load_with_subset_contexts, + load_with_product_context, + load_with_product_contexts, load_container, remove_container, @@ -41,7 +42,7 @@ from .utils import ( from .plugins import ( LoaderPlugin, - SubsetLoaderPlugin, + ProductLoaderPlugin, discover_loader_plugins, register_loader_plugin, @@ -62,13 +63,14 @@ __all__ = ( "LoaderNotFoundError", "get_repres_contexts", - "get_contexts_for_repre_docs", - "get_subset_contexts", + "get_product_contexts", "get_representation_context", + "get_representation_contexts", + "get_representation_contexts_by_ids", "load_with_repre_context", - "load_with_subset_context", - "load_with_subset_contexts", + "load_with_product_context", + "load_with_product_contexts", "load_container", "remove_container", @@ -94,7 +96,7 @@ __all__ = ( # plugins.py "LoaderPlugin", - "SubsetLoaderPlugin", + "ProductLoaderPlugin", "discover_loader_plugins", "register_loader_plugin", diff --git a/client/ayon_core/pipeline/load/plugins.py b/client/ayon_core/pipeline/load/plugins.py index 3b60e357af..b5d228380a 100644 --- a/client/ayon_core/pipeline/load/plugins.py +++ b/client/ayon_core/pipeline/load/plugins.py @@ -33,7 +33,7 @@ class LoaderPlugin(list): options = [] - log = logging.getLogger("SubsetLoader") + log = logging.getLogger("ProductLoader") log.propagate = True @classmethod @@ -76,11 +76,11 @@ class LoaderPlugin(list): setattr(cls, option, value) @classmethod - def has_valid_extension(cls, repre_doc): + def has_valid_extension(cls, repre_entity): """Has representation document valid extension for loader. Args: - repre_doc (dict[str, Any]): Representation document. + repre_entity (dict[str, Any]): Representation entity. Returns: bool: Representation has valid extension @@ -90,11 +90,11 @@ class LoaderPlugin(list): return True # Get representation main file extension from 'context' - repre_context = repre_doc.get("context") or {} + repre_context = repre_entity.get("context") or {} ext = repre_context.get("ext") if not ext: # Legacy way how to get extensions - path = repre_doc.get("data", {}).get("path") + path = repre_entity.get("attrib", {}).get("path") if not path: cls.log.info( "Representation doesn't have known source of extension" @@ -130,46 +130,36 @@ class LoaderPlugin(list): """ plugin_repre_names = cls.get_representations() - plugin_families = cls.families + plugin_product_types = cls.families if ( not plugin_repre_names - or not plugin_families + or not plugin_product_types or not cls.extensions ): return False - repre_doc = context.get("representation") - if not repre_doc: + repre_entity = context.get("representation") + if not repre_entity: return False plugin_repre_names = set(plugin_repre_names) if ( "*" not in plugin_repre_names - and repre_doc["name"] not in plugin_repre_names + and repre_entity["name"] not in plugin_repre_names ): return False - if not cls.has_valid_extension(repre_doc): + if not cls.has_valid_extension(repre_entity): return False - plugin_families = set(plugin_families) - if "*" in plugin_families: + plugin_product_types = set(plugin_product_types) + if "*" in plugin_product_types: return True - subset_doc = context["subset"] - maj_version, _ = schema.get_schema_version(subset_doc["schema"]) - if maj_version < 3: - families = context["version"]["data"].get("families") - else: - families = subset_doc["data"].get("families") - if families is None: - family = subset_doc["data"].get("family") - if family: - families = [family] + product_entity = context["product"] + product_type = product_entity["productType"] - if not families: - return False - return any(family in plugin_families for family in families) + return product_type in plugin_product_types @classmethod def get_representations(cls): @@ -245,7 +235,7 @@ class LoaderPlugin(list): return self._fname -class SubsetLoaderPlugin(LoaderPlugin): +class ProductLoaderPlugin(LoaderPlugin): """Load product into host application Arguments: context (dict): avalon-core:context-1.0 diff --git a/client/ayon_core/pipeline/load/utils.py b/client/ayon_core/pipeline/load/utils.py index e2bc1c7a26..8fdaf52e27 100644 --- a/client/ayon_core/pipeline/load/utils.py +++ b/client/ayon_core/pipeline/load/utils.py @@ -1,29 +1,13 @@ import os import platform -import copy import logging import inspect import collections import numbers +import ayon_api + from ayon_core.host import ILoadHost -from ayon_core.client import ( - get_project, - get_assets, - get_asset_by_id, - get_subsets, - get_subset_by_id, - get_versions, - get_version_by_id, - get_last_version_by_subset_id, - get_hero_version_by_subset_id, - get_version_by_name, - get_last_versions, - get_representations, - get_representation_by_id, - get_representation_by_name, - get_representation_parents -) from ayon_core.lib import ( StringTemplate, TemplateUnsolved, @@ -98,8 +82,8 @@ def get_repres_contexts(representation_ids, project_name=None): Returns: dict: The full representation context by representation id. - keys are repre_id, value is dictionary with full documents of - asset, subset, version and representation. + keys are repre_id, value is dictionary with entities of + folder, product, version and representation. """ from ayon_core.pipeline import get_current_project_name @@ -109,91 +93,20 @@ def get_repres_contexts(representation_ids, project_name=None): if not project_name: project_name = get_current_project_name() - repre_docs = get_representations(project_name, representation_ids) - - return get_contexts_for_repre_docs(project_name, repre_docs) - - -def get_contexts_for_repre_docs(project_name, repre_docs): - contexts = {} - if not repre_docs: - return contexts - - repre_docs_by_id = {} - version_ids = set() - for repre_doc in repre_docs: - version_ids.add(repre_doc["parent"]) - repre_docs_by_id[repre_doc["_id"]] = repre_doc - - version_docs = get_versions( - project_name, version_ids, hero=True + repre_entities = ayon_api.get_representations( + project_name, representation_ids ) - version_docs_by_id = {} - hero_version_docs = [] - versions_for_hero = set() - subset_ids = set() - for version_doc in version_docs: - if version_doc["type"] == "hero_version": - hero_version_docs.append(version_doc) - versions_for_hero.add(version_doc["version_id"]) - version_docs_by_id[version_doc["_id"]] = version_doc - subset_ids.add(version_doc["parent"]) - - if versions_for_hero: - _version_docs = get_versions(project_name, versions_for_hero) - _version_data_by_id = { - version_doc["_id"]: version_doc["data"] - for version_doc in _version_docs - } - - for hero_version_doc in hero_version_docs: - hero_version_id = hero_version_doc["_id"] - version_id = hero_version_doc["version_id"] - version_data = copy.deepcopy(_version_data_by_id[version_id]) - version_docs_by_id[hero_version_id]["data"] = version_data - - subset_docs = get_subsets(project_name, subset_ids) - subset_docs_by_id = {} - asset_ids = set() - for subset_doc in subset_docs: - subset_docs_by_id[subset_doc["_id"]] = subset_doc - asset_ids.add(subset_doc["parent"]) - - asset_docs = get_assets(project_name, asset_ids) - asset_docs_by_id = { - asset_doc["_id"]: asset_doc - for asset_doc in asset_docs - } - - project_doc = get_project(project_name) - - for repre_id, repre_doc in repre_docs_by_id.items(): - version_doc = version_docs_by_id[repre_doc["parent"]] - subset_doc = subset_docs_by_id[version_doc["parent"]] - asset_doc = asset_docs_by_id[subset_doc["parent"]] - context = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code") - }, - "asset": asset_doc, - "subset": subset_doc, - "version": version_doc, - "representation": repre_doc, - } - contexts[repre_id] = context - - return contexts + return get_representation_contexts(project_name, repre_entities) -def get_subset_contexts(subset_ids, project_name=None): - """Return parenthood context for subset. +def get_product_contexts(product_ids, project_name=None): + """Return parenthood context for product. - Provides context on subset granularity - less detail than + Provides context on product granularity - less detail than 'get_repre_contexts'. Args: - subset_ids (list): The subset ids. + product_ids (list): The product ids. project_name (Optional[str]): Project name. Returns: dict: The full representation context by representation id. @@ -201,86 +114,173 @@ def get_subset_contexts(subset_ids, project_name=None): from ayon_core.pipeline import get_current_project_name contexts = {} - if not subset_ids: + if not product_ids: return contexts if not project_name: project_name = get_current_project_name() - subset_docs = get_subsets(project_name, subset_ids) - subset_docs_by_id = {} - asset_ids = set() - for subset_doc in subset_docs: - subset_docs_by_id[subset_doc["_id"]] = subset_doc - asset_ids.add(subset_doc["parent"]) + product_entities = ayon_api.get_products( + project_name, product_ids=product_ids + ) + product_entities_by_id = {} + folder_ids = set() + for product_entity in product_entities: + product_entities_by_id[product_entity["id"]] = product_entity + folder_ids.add(product_entity["folderId"]) - asset_docs = get_assets(project_name, asset_ids) - asset_docs_by_id = { - asset_doc["_id"]: asset_doc - for asset_doc in asset_docs + folder_entities_by_id = { + folder_entity["id"]: folder_entity + for folder_entity in ayon_api.get_folders( + project_name, folder_ids=folder_ids + ) } - project_doc = get_project(project_name) + project_entity = ayon_api.get_project(project_name) - for subset_id, subset_doc in subset_docs_by_id.items(): - asset_doc = asset_docs_by_id[subset_doc["parent"]] + for product_id, product_entity in product_entities_by_id.items(): + folder_entity = folder_entities_by_id[product_entity["folderId"]] context = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"].get("code") - }, - "asset": asset_doc, - "subset": subset_doc + "project": project_entity, + "folder": folder_entity, + "product": product_entity } - contexts[subset_id] = context + contexts[product_id] = context return contexts -def get_representation_context(representation): +def get_representation_contexts(project_name, representation_entities): + """Parenthood context for representations. + + Function fills ``None`` if any entity was not found or could + not be queried. + + Args: + project_name (str): Project name. + representation_entities (Iterable[dict[str, Any]]): Representation + entities. + + Returns: + dict[str, dict[str, Any]]: The full representation context by + representation id. + + """ + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in representation_entities + } + + if not repre_entities_by_id: + return {} + + repre_ids = set(repre_entities_by_id) + + parents_by_repre_id = ayon_api.get_representations_parents( + project_name, repre_ids + ) + output = {} + for repre_id in repre_ids: + repre_entity = repre_entities_by_id[repre_id] + ( + version_entity, + product_entity, + folder_entity, + project_entity + ) = parents_by_repre_id[repre_id] + output[repre_id] = { + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, + "representation": repre_entity, + } + return output + + +def get_representation_contexts_by_ids(project_name, representation_ids): + """Parenthood context for representations found by ids. + + Function fills ``None`` if any entity was not found or could + not be queried. + + Args: + project_name (str): Project name. + representation_ids (Iterable[str]): Representation ids. + + Returns: + dict[str, dict[str, Any]]: The full representation context by + representation id. + + """ + repre_ids = set(representation_ids) + if not repre_ids: + return {} + + # Query representation entities by id + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in ayon_api.get_representations( + project_name, repre_ids + ) + } + output = get_representation_contexts( + project_name, repre_entities_by_id.values() + ) + for repre_id in repre_ids: + if repre_id not in output: + output[repre_id] = { + "project": None, + "folder": None, + "product": None, + "version": None, + "representation": None, + } + return output + + +def get_representation_context(project_name, representation): """Return parenthood context for representation. Args: - representation (str or ObjectId or dict): The representation id - or full representation as returned by the database. + project_name (str): Project name. + representation (Union[dict[str, Any], str]): Representation entity + or representation id. Returns: - dict: The full representation context. + dict[str, dict[str, Any]]: The full representation context. + + Raises: + ValueError: When representation is invalid or parents were not found. + """ - from ayon_core.pipeline import get_current_project_name - - assert representation is not None, "This is a bug" - - project_name = get_current_project_name() - if not isinstance(representation, dict): - representation = get_representation_by_id( - project_name, representation + if not representation: + raise ValueError( + "Invalid argument value {}".format(str(representation)) ) - if not representation: - raise AssertionError("Representation was not found in database") + if isinstance(representation, dict): + repre_entity = representation + repre_id = repre_entity["id"] + context = get_representation_contexts( + project_name, [repre_entity] + )[repre_id] + else: + repre_id = representation + context = get_representation_contexts_by_ids( + project_name, {repre_id} + )[repre_id] - version, subset, asset, project = get_representation_parents( - project_name, representation - ) - if not version: - raise AssertionError("Version was not found in database") - if not subset: - raise AssertionError("Subset was not found in database") - if not asset: - raise AssertionError("Asset was not found in database") - if not project: - raise AssertionError("Project was not found in database") + missing_entities = [] + for key, value in context.items(): + if value is None: + missing_entities.append(key) - context = { - "project": { - "name": project["name"], - "code": project["data"].get("code", '') - }, - "asset": asset, - "subset": subset, - "version": version, - "representation": representation, - } + if missing_entities: + raise ValueError( + "Not able to receive representation parent types: {}".format( + ", ".join(missing_entities) + ) + ) return context @@ -293,7 +293,7 @@ def load_with_repre_context( if not is_compatible_loader(Loader, repre_context): raise IncompatibleLoaderError( "Loader {} is incompatible with {}".format( - Loader.__name__, repre_context["subset"]["name"] + Loader.__name__, repre_context["product"]["name"] ) ) @@ -303,13 +303,13 @@ def load_with_repre_context( assert isinstance(options, dict), "Options must be a dictionary" - # Fallback to subset when name is None + # Fallback to product when name is None if name is None: - name = repre_context["subset"]["name"] + name = repre_context["product"]["name"] log.info( "Running '%s' on '%s'" % ( - Loader.__name__, repre_context["asset"]["name"] + Loader.__name__, repre_context["folder"]["path"] ) ) @@ -323,8 +323,8 @@ def load_with_repre_context( return loader.load(repre_context, name, namespace, options) -def load_with_subset_context( - Loader, subset_context, namespace=None, name=None, options=None, **kwargs +def load_with_product_context( + Loader, product_context, namespace=None, name=None, options=None, **kwargs ): # Ensure options is a dictionary when no explicit options provided @@ -333,21 +333,21 @@ def load_with_subset_context( assert isinstance(options, dict), "Options must be a dictionary" - # Fallback to subset when name is None + # Fallback to product when name is None if name is None: - name = subset_context["subset"]["name"] + name = product_context["product"]["name"] log.info( "Running '%s' on '%s'" % ( - Loader.__name__, subset_context["asset"]["name"] + Loader.__name__, product_context["folder"]["path"] ) ) - return Loader().load(subset_context, name, namespace, options) + return Loader().load(product_context, name, namespace, options) -def load_with_subset_contexts( - Loader, subset_contexts, namespace=None, name=None, options=None, **kwargs +def load_with_product_contexts( + Loader, product_contexts, namespace=None, name=None, options=None, **kwargs ): # Ensure options is a dictionary when no explicit options provided @@ -356,19 +356,21 @@ def load_with_subset_contexts( assert isinstance(options, dict), "Options must be a dictionary" - # Fallback to subset when name is None - joined_subset_names = " | ".join( - context["subset"]["name"] - for context in subset_contexts + # Fallback to product when name is None + joined_product_names = " | ".join( + context["product"]["name"] + for context in product_contexts ) if name is None: - name = joined_subset_names + name = joined_product_names log.info( - "Running '{}' on '{}'".format(Loader.__name__, joined_subset_names) + "Running '{}' on '{}'".format( + Loader.__name__, joined_product_names + ) ) - return Loader().load(subset_contexts, name, namespace, options) + return Loader().load(product_contexts, name, namespace, options) def load_container( @@ -378,10 +380,10 @@ def load_container( Args: Loader (Loader): The loader class to trigger. - representation (str or ObjectId or dict): The representation id + representation (str or dict): The representation id or full representation as returned by the database. namespace (str, Optional): The namespace to assign. Defaults to None. - name (str, Optional): The name to assign. Defaults to subset name. + name (str, Optional): The name to assign. Defaults to product name. options (dict, Optional): Additional options to pass on to the loader. Returns: @@ -392,8 +394,11 @@ def load_container( the representation. """ + from ayon_core.pipeline import get_current_project_name - context = get_representation_context(representation) + context = get_representation_context( + get_current_project_name(), representation + ) return load_with_repre_context( Loader, context, @@ -459,41 +464,54 @@ def update_container(container, version=-1): # Compute the different version from 'representation' project_name = get_current_project_name() - current_representation = get_representation_by_id( + current_representation = ayon_api.get_representation_by_id( project_name, container["representation"] ) assert current_representation is not None, "This is a bug" - current_version = get_version_by_id( - project_name, current_representation["parent"], fields=["parent"] + current_version_id = current_representation["versionId"] + current_version = ayon_api.get_version_by_id( + project_name, current_version_id, fields={"productId"} ) - if version == -1: - new_version = get_last_version_by_subset_id( - project_name, current_version["parent"], fields=["_id"] + if isinstance(version, HeroVersionType): + new_version = ayon_api.get_hero_version_by_product_id( + project_name, current_version["productId"] ) - - elif isinstance(version, HeroVersionType): - new_version = get_hero_version_by_subset_id( - project_name, current_version["parent"], fields=["_id"] + elif version == -1: + new_version = ayon_api.get_last_version_by_product_id( + project_name, current_version["productId"] ) else: - new_version = get_version_by_name( - project_name, version, current_version["parent"], fields=["_id"] + new_version = ayon_api.get_version_by_name( + project_name, version, current_version["productId"] ) - subset_doc = get_subset_by_id(project_name, current_version["parent"]) - asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) - assert new_version is not None, "This is a bug" + if new_version is None: + raise ValueError("Failed to find matching version") - new_representation = get_representation_by_name( - project_name, current_representation["name"], new_version["_id"] + product_entity = ayon_api.get_product_by_id( + project_name, current_version["productId"] ) - assert new_representation is not None, "Representation wasn't found" + folder_entity = ayon_api.get_folder_by_id( + project_name, product_entity["folderId"] + ) + + repre_name = current_representation["name"] + new_representation = ayon_api.get_representation_by_name( + project_name, repre_name, new_version["id"] + ) + if new_representation is None: + raise ValueError( + "Representation '{}' wasn't found on requested version".format( + repre_name + ) + ) path = get_representation_path(new_representation) - assert os.path.exists(path), "Path {} doesn't exist".format(path) + if not path or not os.path.exists(path): + raise ValueError("Path {} doesn't exist".format(path)) # Run update on the Loader for this container Loader = _get_container_loader(container) @@ -502,14 +520,11 @@ def update_container(container, version=-1): "Can't update container because loader '{}' was not found." .format(container.get("loader")) ) - project_doc = get_project(project_name) + project_entity = ayon_api.get_project(project_name) context = { - "project": { - "name": project_doc["name"], - "code": project_doc["data"]["code"], - }, - "asset": asset_doc, - "subset": subset_doc, + "project": project_entity, + "folder": folder_entity, + "product": product_entity, "version": new_version, "representation": new_representation, } @@ -522,7 +537,7 @@ def switch_container(container, representation, loader_plugin=None): Args: container (dict): container information - representation (dict): representation data from document + representation (dict): representation entity Returns: function call @@ -549,21 +564,20 @@ def switch_container(container, representation, loader_plugin=None): # Get the new representation to switch to project_name = get_current_project_name() - new_representation = get_representation_by_id( - project_name, representation["_id"] - ) - new_context = get_representation_context(new_representation) - if not is_compatible_loader(loader_plugin, new_context): + context = get_representation_context( + project_name, representation["id"] + ) + if not is_compatible_loader(loader_plugin, context): raise IncompatibleLoaderError( "Loader {} is incompatible with {}".format( - loader_plugin.__name__, new_context["subset"]["name"] + loader_plugin.__name__, context["product"]["name"] ) ) - loader = loader_plugin(new_context) + loader = loader_plugin(context) - return loader.switch(container, new_context) + return loader.switch(container, context) def get_representation_path_from_context(context): @@ -571,16 +585,19 @@ def get_representation_path_from_context(context): from ayon_core.pipeline import get_current_project_name representation = context["representation"] - project_doc = context.get("project") + project_entity = context.get("project") root = None - if project_doc and project_doc["name"] != get_current_project_name(): - anatomy = Anatomy(project_doc["name"]) + if ( + project_entity + and project_entity["name"] != get_current_project_name() + ): + anatomy = Anatomy(project_entity["name"]) root = anatomy.roots return get_representation_path(representation, root) -def get_representation_path_with_anatomy(repre_doc, anatomy): +def get_representation_path_with_anatomy(repre_entity, anatomy): """Receive representation path using representation document and anatomy. Anatomy is used to replace 'root' key in representation file. Ideally @@ -592,7 +609,7 @@ def get_representation_path_with_anatomy(repre_doc, anatomy): imagine the result should also contain paths to possible resources. Args: - repre_doc (Dict[str, Any]): Representation document. + repre_entity (Dict[str, Any]): Representation entity. anatomy (Anatomy): Project anatomy object. Returns: @@ -604,7 +621,7 @@ def get_representation_path_with_anatomy(repre_doc, anatomy): """ try: - template = repre_doc["data"]["template"] + template = repre_entity["attrib"]["template"] except KeyError: raise InvalidRepresentationContext(( @@ -613,7 +630,7 @@ def get_representation_path_with_anatomy(repre_doc, anatomy): )) try: - context = repre_doc["context"] + context = repre_entity["context"] context["root"] = anatomy.roots path = StringTemplate.format_strict_template(template, context) @@ -651,7 +668,7 @@ def get_representation_path(representation, root=None): def path_from_representation(): try: - template = representation["data"]["template"] + template = representation["attrib"]["template"] except KeyError: return None @@ -678,10 +695,10 @@ def get_representation_path(representation, root=None): return path def path_from_data(): - if "path" not in representation["data"]: + if "path" not in representation["attrib"]: return None - path = representation["data"]["path"] + path = representation["attrib"]["path"] # Force replacing backslashes with forward slashed if not on # windows if platform.system().lower() != "windows": @@ -758,8 +775,12 @@ def filter_repre_contexts_by_loader(repre_contexts, loader): def loaders_from_representation(loaders, representation): """Return all compatible loaders for a representation.""" + from ayon_core.pipeline import get_current_project_name - context = get_representation_context(representation) + project_name = get_current_project_name() + context = get_representation_context( + project_name, representation + ) return loaders_from_repre_context(loaders, context) @@ -838,55 +859,57 @@ def filter_containers(containers, project_name): invalid_containers.extend(containers) return output - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_ids=repre_ids, - fields=["_id", "parent"] + fields={"id", "versionId"} ) # Store representations by stringified representation id - repre_docs_by_str_id = {} - repre_docs_by_version_id = collections.defaultdict(list) - for repre_doc in repre_docs: - repre_id = str(repre_doc["_id"]) - version_id = repre_doc["parent"] - repre_docs_by_str_id[repre_id] = repre_doc - repre_docs_by_version_id[version_id].append(repre_doc) + repre_entities_by_id = {} + repre_entities_by_version_id = collections.defaultdict(list) + for repre_entity in repre_entities: + repre_id = repre_entity["id"] + version_id = repre_entity["versionId"] + repre_entities_by_id[repre_id] = repre_entity + repre_entities_by_version_id[version_id].append(repre_entity) - # Query version docs to get it's subset ids + # Query version docs to get it's product ids # - also query hero version to be able identify if representation # belongs to existing version - version_docs = get_versions( + version_entities = ayon_api.get_versions( project_name, - version_ids=repre_docs_by_version_id.keys(), + version_ids=repre_entities_by_version_id.keys(), hero=True, - fields=["_id", "parent", "type"] + fields={"id", "productId", "version"} ) verisons_by_id = {} - versions_by_subset_id = collections.defaultdict(list) + versions_by_product_id = collections.defaultdict(list) hero_version_ids = set() - for version_doc in version_docs: - version_id = version_doc["_id"] + for version_entity in version_entities: + version_id = version_entity["id"] # Store versions by their ids - verisons_by_id[version_id] = version_doc - # There's no need to query subsets for hero versions + verisons_by_id[version_id] = version_entity + # There's no need to query products for hero versions # - they are considered as latest? - if version_doc["type"] == "hero_version": + if version_entity["version"] < 0: hero_version_ids.add(version_id) continue - subset_id = version_doc["parent"] - versions_by_subset_id[subset_id].append(version_doc) + product_id = version_entity["productId"] + versions_by_product_id[product_id].append(version_entity) - last_versions = get_last_versions( + last_versions = ayon_api.get_last_versions( project_name, - subset_ids=versions_by_subset_id.keys(), - fields=["_id"] + versions_by_product_id.keys(), + fields={"id"} ) # Figure out which versions are outdated outdated_version_ids = set() - for subset_id, last_version_doc in last_versions.items(): - for version_doc in versions_by_subset_id[subset_id]: - version_id = version_doc["_id"] - if version_id != last_version_doc["_id"]: + for product_id, last_version_entity in last_versions.items(): + for version_entity in versions_by_product_id[product_id]: + version_id = version_entity["id"] + if version_id in hero_version_ids: + continue + if version_id != last_version_entity["id"]: outdated_version_ids.add(version_id) # Based on all collected data figure out which containers are outdated @@ -898,8 +921,8 @@ def filter_containers(containers, project_name): invalid_containers.append(container) continue - repre_doc = repre_docs_by_str_id.get(repre_id) - if not repre_doc: + repre_entity = repre_entities_by_id.get(repre_id) + if not repre_entity: log.debug(( "Container '{}' has an invalid representation." " It is missing in the database." @@ -907,7 +930,7 @@ def filter_containers(containers, project_name): not_found_containers.append(container) continue - version_id = repre_doc["parent"] + version_id = repre_entity["versionId"] if version_id in outdated_version_ids: outdated_containers.append(container) diff --git a/client/ayon_core/pipeline/schema/__init__.py b/client/ayon_core/pipeline/schema/__init__.py index d7b33f2621..3abc576f89 100644 --- a/client/ayon_core/pipeline/schema/__init__.py +++ b/client/ayon_core/pipeline/schema/__init__.py @@ -29,34 +29,6 @@ CURRENT_DIR = os.path.dirname(os.path.abspath(__file__)) _CACHED = False -def get_schema_version(schema_name): - """Extract version form schema name. - - It is expected that schema name contain only major and minor version. - - Expected name should match to: - "{name}:{type}-{major version}.{minor version}" - - `name` - must not contain colon - - `type` - must not contain dash - - major and minor versions must be numbers separated by dot - - Args: - schema_name(str): Name of schema that should be parsed. - - Returns: - tuple: Contain two values major version as first and minor version as - second. When schema does not match parsing regex then `(0, 0)` is - returned. - """ - schema_regex = re.compile(r"[^:]+:[^-]+-(\d.\d)") - groups = schema_regex.findall(schema_name) - if not groups: - return 0, 0 - - maj_version, min_version = groups[0].split(".") - return int(maj_version), int(min_version) - - def validate(data, schema=None): """Validate `data` with `schema` diff --git a/client/ayon_core/pipeline/schema/application-1.0.json b/client/ayon_core/pipeline/schema/application-1.0.json deleted file mode 100644 index 953abee569..0000000000 --- a/client/ayon_core/pipeline/schema/application-1.0.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:application-1.0", - "description": "An application definition.", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "label", - "application_dir", - "executable" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "label": { - "description": "Nice name of application.", - "type": "string" - }, - "application_dir": { - "description": "Name of directory used for application resources.", - "type": "string" - }, - "executable": { - "description": "Name of callable executable, this is called to launch the application", - "type": "string" - }, - "description": { - "description": "Description of application.", - "type": "string" - }, - "environment": { - "description": "Key/value pairs for environment variables related to this application. Supports lists for paths, such as PYTHONPATH.", - "type": "object", - "items": { - "oneOf": [ - {"type": "string"}, - {"type": "array", "items": {"type": "string"}} - ] - } - }, - "default_dirs": { - "type": "array", - "items": { - "type": "string" - } - }, - "copy": { - "type": "object", - "patternProperties": { - "^.*$": { - "anyOf": [ - {"type": "string"}, - {"type": "null"} - ] - } - }, - "additionalProperties": false - } - } -} diff --git a/client/ayon_core/pipeline/schema/asset-1.0.json b/client/ayon_core/pipeline/schema/asset-1.0.json deleted file mode 100644 index ab104c002a..0000000000 --- a/client/ayon_core/pipeline/schema/asset-1.0.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:asset-1.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "name", - "subsets" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "name": { - "description": "Name of directory", - "type": "string" - }, - "subsets": { - "type": "array", - "items": { - "$ref": "subset.json" - } - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/asset-2.0.json b/client/ayon_core/pipeline/schema/asset-2.0.json deleted file mode 100644 index b894d79792..0000000000 --- a/client/ayon_core/pipeline/schema/asset-2.0.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:asset-2.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "silo", - "data" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:asset-2.0"], - "example": "openpype:asset-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["asset"], - "example": "asset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of asset", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "Bruce" - }, - "silo": { - "description": "Group or container of asset", - "type": "string", - "example": "assets" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": {"key": "value"} - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/asset-3.0.json b/client/ayon_core/pipeline/schema/asset-3.0.json deleted file mode 100644 index 948704d2a1..0000000000 --- a/client/ayon_core/pipeline/schema/asset-3.0.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:asset-3.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:asset-3.0"], - "example": "openpype:asset-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["asset"], - "example": "asset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of asset", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "Bruce" - }, - "silo": { - "description": "Group or container of asset", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "assets" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": {"key": "value"} - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/config-1.0.json b/client/ayon_core/pipeline/schema/config-1.0.json deleted file mode 100644 index 49398a57cd..0000000000 --- a/client/ayon_core/pipeline/schema/config-1.0.json +++ /dev/null @@ -1,85 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.0", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": false, - "required": [ - "tasks", - "apps" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "template": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^.*$": { - "type": "string" - } - } - }, - "tasks": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "apps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "families": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "label": {"type": "string"}, - "hideFilter": {"type": "boolean"} - }, - "required": ["name"] - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "color": {"type": "string"}, - "order": {"type": ["integer", "number"]} - }, - "required": ["name"] - } - }, - "copy": { - "type": "object" - } - } -} diff --git a/client/ayon_core/pipeline/schema/config-1.1.json b/client/ayon_core/pipeline/schema/config-1.1.json deleted file mode 100644 index 6e15514aaf..0000000000 --- a/client/ayon_core/pipeline/schema/config-1.1.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.1", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": false, - "required": [ - "tasks", - "apps" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "template": { - "type": "object", - "additionalProperties": false, - "patternProperties": { - "^.*$": { - "type": "string" - } - } - }, - "tasks": { - "type": "object", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": [ - "short_name" - ] - } - }, - "apps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "families": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "label": {"type": "string"}, - "hideFilter": {"type": "boolean"} - }, - "required": ["name"] - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "color": {"type": "string"}, - "order": {"type": ["integer", "number"]} - }, - "required": ["name"] - } - }, - "copy": { - "type": "object" - } - } -} diff --git a/client/ayon_core/pipeline/schema/config-2.0.json b/client/ayon_core/pipeline/schema/config-2.0.json deleted file mode 100644 index 54b226711a..0000000000 --- a/client/ayon_core/pipeline/schema/config-2.0.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-2.0", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": false, - "required": [ - "tasks", - "apps" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "templates": { - "type": "object" - }, - "roots": { - "type": "object" - }, - "imageio": { - "type": "object" - }, - "tasks": { - "type": "object", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": [ - "short_name" - ] - } - }, - "apps": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "group": {"type": "string"}, - "label": {"type": "string"} - }, - "required": ["name"] - } - }, - "families": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "label": {"type": "string"}, - "hideFilter": {"type": "boolean"} - }, - "required": ["name"] - } - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "name": {"type": "string"}, - "icon": {"type": "string"}, - "color": {"type": "string"}, - "order": {"type": ["integer", "number"]} - }, - "required": ["name"] - } - }, - "copy": { - "type": "object" - } - } -} diff --git a/client/ayon_core/pipeline/schema/hero_version-1.0.json b/client/ayon_core/pipeline/schema/hero_version-1.0.json deleted file mode 100644 index b720dc2887..0000000000 --- a/client/ayon_core/pipeline/schema/hero_version-1.0.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:hero_version-1.0", - "description": "Hero version of asset", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "version_id", - "schema", - "type", - "parent" - ], - - "properties": { - "_id": { - "description": "Document's id (database will create it's if not entered)", - "example": "ObjectId(592c33475f8c1b064c4d1696)" - }, - "version_id": { - "description": "The version ID from which it was created", - "example": "ObjectId(592c33475f8c1b064c4d1695)" - }, - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:hero_version-1.0"], - "example": "openpype:hero_version-1.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["hero_version"], - "example": "hero_version" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "ObjectId(592c33475f8c1b064c4d1697)" - } - } -} diff --git a/client/ayon_core/pipeline/schema/inventory-1.0.json b/client/ayon_core/pipeline/schema/inventory-1.0.json deleted file mode 100644 index 2fe78794ab..0000000000 --- a/client/ayon_core/pipeline/schema/inventory-1.0.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.0", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": true -} diff --git a/client/ayon_core/pipeline/schema/inventory-1.1.json b/client/ayon_core/pipeline/schema/inventory-1.1.json deleted file mode 100644 index b61a76b32a..0000000000 --- a/client/ayon_core/pipeline/schema/inventory-1.1.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:config-1.1", - "description": "A project configuration.", - - "type": "object", - - "additionalProperties": true -} diff --git a/client/ayon_core/pipeline/schema/project-2.0.json b/client/ayon_core/pipeline/schema/project-2.0.json deleted file mode 100644 index 0ed5a55599..0000000000 --- a/client/ayon_core/pipeline/schema/project-2.0.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:project-2.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data", - "config" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:project-2.0"], - "example": "openpype:project-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["project"], - "example": "project" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "hulk" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "fps": 24, - "width": 1920, - "height": 1080 - } - }, - "config": { - "type": "object", - "description": "Document metadata", - "example": { - "schema": "openpype:config-1.0", - "apps": [ - { - "name": "maya2016", - "label": "Autodesk Maya 2016" - }, - { - "name": "nuke10", - "label": "The Foundry Nuke 10.0" - } - ], - "tasks": [ - {"name": "model"}, - {"name": "render"}, - {"name": "animate"}, - {"name": "rig"}, - {"name": "lookdev"}, - {"name": "layout"} - ], - "template": { - "work": - "{root}/{project}/{silo}/{asset}/work/{task}/{app}", - "publish": - "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}" - } - }, - "$ref": "config-1.0.json" - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/project-2.1.json b/client/ayon_core/pipeline/schema/project-2.1.json deleted file mode 100644 index 9413c9f691..0000000000 --- a/client/ayon_core/pipeline/schema/project-2.1.json +++ /dev/null @@ -1,86 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:project-2.1", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data", - "config" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:project-2.1"], - "example": "openpype:project-2.1" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["project"], - "example": "project" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "hulk" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "fps": 24, - "width": 1920, - "height": 1080 - } - }, - "config": { - "type": "object", - "description": "Document metadata", - "example": { - "schema": "openpype:config-1.1", - "apps": [ - { - "name": "maya2016", - "label": "Autodesk Maya 2016" - }, - { - "name": "nuke10", - "label": "The Foundry Nuke 10.0" - } - ], - "tasks": { - "Model": {"short_name": "mdl"}, - "Render": {"short_name": "rnd"}, - "Animate": {"short_name": "anim"}, - "Rig": {"short_name": "rig"}, - "Lookdev": {"short_name": "look"}, - "Layout": {"short_name": "lay"} - }, - "template": { - "work": - "{root}/{project}/{silo}/{asset}/work/{task}/{app}", - "publish": - "{root}/{project}/{silo}/{asset}/publish/{subset}/v{version:0>3}/{subset}.{representation}" - } - }, - "$ref": "config-1.1.json" - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/project-3.0.json b/client/ayon_core/pipeline/schema/project-3.0.json deleted file mode 100644 index be23e10c93..0000000000 --- a/client/ayon_core/pipeline/schema/project-3.0.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:project-3.0", - "description": "A unit of data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "name", - "data", - "config" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:project-3.0"], - "example": "openpype:project-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["project"], - "example": "project" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "hulk" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "fps": 24, - "width": 1920, - "height": 1080 - } - }, - "config": { - "type": "object", - "description": "Document metadata", - "$ref": "config-2.0.json" - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/representation-1.0.json b/client/ayon_core/pipeline/schema/representation-1.0.json deleted file mode 100644 index 347c585f52..0000000000 --- a/client/ayon_core/pipeline/schema/representation-1.0.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:representation-1.0", - "description": "The inverse of an instance", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "format", - "path" - ], - - "properties": { - "schema": {"type": "string"}, - "format": { - "description": "File extension, including '.'", - "type": "string" - }, - "path": { - "description": "Unformatted path to version.", - "type": "string" - } - } -} diff --git a/client/ayon_core/pipeline/schema/representation-2.0.json b/client/ayon_core/pipeline/schema/representation-2.0.json deleted file mode 100644 index f47c16a10a..0000000000 --- a/client/ayon_core/pipeline/schema/representation-2.0.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:representation-2.0", - "description": "The inverse of an instance", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:representation-2.0"], - "example": "openpype:representation-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["representation"], - "example": "representation" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of representation", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "abc" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": { - "label": "Alembic" - } - }, - "dependencies": { - "description": "Other representation that this representation depends on", - "type": "array", - "items": {"type": "string"}, - "example": [ - "592d547a5f8c1b388093c145" - ] - }, - "context": { - "description": "Summary of the context to which this representation belong.", - "type": "object", - "properties": { - "project": {"type": "object"}, - "asset": {"type": "string"}, - "silo": {"type": ["string", "null"]}, - "subset": {"type": "string"}, - "version": {"type": "number"}, - "representation": {"type": "string"} - }, - "example": { - "project": "hulk", - "asset": "Bruce", - "silo": "assets", - "subset": "rigDefault", - "version": 12, - "representation": "ma" - } - } - } -} diff --git a/client/ayon_core/pipeline/schema/session-1.0.json b/client/ayon_core/pipeline/schema/session-1.0.json deleted file mode 100644 index 5ced0a6f08..0000000000 --- a/client/ayon_core/pipeline/schema/session-1.0.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-1.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECTS", - "AVALON_PROJECT", - "AVALON_ASSET", - "AVALON_SILO", - "AVALON_CONFIG" - ], - - "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^\\w*$", - "example": "Bruce" - }, - "AVALON_SILO": { - "description": "Name of asset group or container", - "type": "string", - "pattern": "^\\w*$", - "example": "assets" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_CONFIG": { - "description": "Name of Avalon configuration", - "type": "string", - "pattern": "^\\w*$", - "example": "polly" - }, - "AVALON_APP": { - "description": "Name of application", - "type": "string", - "pattern": "^\\w*$", - "example": "maya2016" - }, - "AVALON_MONGO": { - "description": "Address to the asset database", - "type": "string", - "pattern": "^mongodb://[\\w/@:.]*$", - "example": "mongodb://localhost:27017", - "default": "mongodb://localhost:27017" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "Mindbender", - "default": "Avalon" - }, - "AVALON_SENTRY": { - "description": "Address to Sentry", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", - "default": null - }, - "AVALON_DEADLINE": { - "description": "Address to Deadline", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "http://192.168.99.101", - "default": null - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - }, - "AVALON_UPLOAD": { - "description": "Boolean of whether to upload published material to central asset repository", - "type": "string", - "default": null, - "example": "True" - }, - "AVALON_USERNAME": { - "description": "Generic username", - "type": "string", - "pattern": "^\\w*$", - "default": "avalon", - "example": "myself" - }, - "AVALON_PASSWORD": { - "description": "Generic password", - "type": "string", - "pattern": "^\\w*$", - "default": "secret", - "example": "abc123" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" - }, - "AVALON_DEBUG": { - "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", - "type": "string", - "default": null, - "example": "True" - } - } -} diff --git a/client/ayon_core/pipeline/schema/session-2.0.json b/client/ayon_core/pipeline/schema/session-2.0.json deleted file mode 100644 index 0a4d51beb2..0000000000 --- a/client/ayon_core/pipeline/schema/session-2.0.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-2.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECT", - "AVALON_ASSET", - "AVALON_CONFIG" - ], - - "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^\\w*$", - "example": "Bruce" - }, - "AVALON_SILO": { - "description": "Name of asset group or container", - "type": "string", - "pattern": "^\\w*$", - "example": "assets" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_CONFIG": { - "description": "Name of Avalon configuration", - "type": "string", - "pattern": "^\\w*$", - "example": "polly" - }, - "AVALON_APP": { - "description": "Name of application", - "type": "string", - "pattern": "^\\w*$", - "example": "maya2016" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "Mindbender", - "default": "Avalon" - }, - "AVALON_SENTRY": { - "description": "Address to Sentry", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "https://5b872b280de742919b115bdc8da076a5:8d278266fe764361b8fa6024af004a9c@logs.mindbender.com/2", - "default": null - }, - "AVALON_DEADLINE": { - "description": "Address to Deadline", - "type": "string", - "pattern": "^http[\\w/@:.]*$", - "example": "http://192.168.99.101", - "default": null - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - }, - "AVALON_UPLOAD": { - "description": "Boolean of whether to upload published material to central asset repository", - "type": "string", - "default": null, - "example": "True" - }, - "AVALON_USERNAME": { - "description": "Generic username", - "type": "string", - "pattern": "^\\w*$", - "default": "avalon", - "example": "myself" - }, - "AVALON_PASSWORD": { - "description": "Generic password", - "type": "string", - "pattern": "^\\w*$", - "default": "secret", - "example": "abc123" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" - }, - "AVALON_DEBUG": { - "description": "Enable debugging mode. Some applications may use this for e.g. extended verbosity or mock plug-ins.", - "type": "string", - "default": null, - "example": "True" - } - } -} diff --git a/client/ayon_core/pipeline/schema/session-3.0.json b/client/ayon_core/pipeline/schema/session-3.0.json deleted file mode 100644 index 9f785939e4..0000000000 --- a/client/ayon_core/pipeline/schema/session-3.0.json +++ /dev/null @@ -1,81 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-3.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECT", - "AVALON_ASSET" - ], - - "properties": { - "AVALON_PROJECTS": { - "description": "Absolute path to root of project directories", - "type": "string", - "example": "/nas/projects" - }, - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^\\w*$", - "example": "Bruce" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_APP": { - "description": "Name of host", - "type": "string", - "pattern": "^\\w*$", - "example": "maya2016" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "Mindbender", - "default": "Avalon" - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - }, - "AVALON_INSTANCE_ID": { - "description": "Unique identifier for instances in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.instance", - "example": "avalon.instance" - }, - "AVALON_CONTAINER_ID": { - "description": "Unique identifier for a loaded representation in a working file", - "type": "string", - "pattern": "^[\\w.]*$", - "default": "avalon.container", - "example": "avalon.container" - } - } -} diff --git a/client/ayon_core/pipeline/schema/session-4.0.json b/client/ayon_core/pipeline/schema/session-4.0.json deleted file mode 100644 index 0dab48aa46..0000000000 --- a/client/ayon_core/pipeline/schema/session-4.0.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:session-4.0", - "description": "The Avalon environment", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "AVALON_PROJECT" - ], - - "properties": { - "AVALON_PROJECT": { - "description": "Name of project", - "type": "string", - "pattern": "^\\w*$", - "example": "Hulk" - }, - "AVALON_ASSET": { - "description": "Name of asset", - "type": "string", - "pattern": "^[\\/\\w]*$", - "example": "Bruce" - }, - "AVALON_TASK": { - "description": "Name of task", - "type": "string", - "pattern": "^\\w*$", - "example": "modeling" - }, - "AVALON_APP": { - "description": "Name of host", - "type": "string", - "pattern": "^\\w*$", - "example": "maya" - }, - "AVALON_DB": { - "description": "Name of database", - "type": "string", - "pattern": "^\\w*$", - "example": "avalon", - "default": "avalon" - }, - "AVALON_LABEL": { - "description": "Nice name of Avalon, used in e.g. graphical user interfaces", - "type": "string", - "example": "MyLabel", - "default": "Avalon" - }, - "AVALON_TIMEOUT": { - "description": "Wherever there is a need for a timeout, this is the default value.", - "type": "string", - "pattern": "^[0-9]*$", - "default": "1000", - "example": "1000" - } - } -} diff --git a/client/ayon_core/pipeline/schema/shaders-1.0.json b/client/ayon_core/pipeline/schema/shaders-1.0.json deleted file mode 100644 index 7102ba1861..0000000000 --- a/client/ayon_core/pipeline/schema/shaders-1.0.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:shaders-1.0", - "description": "Relationships between shaders and Avalon IDs", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "shader" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "shader": { - "description": "Name of directory", - "type": "array", - "items": { - "type": "str", - "description": "Avalon ID and optional face indexes, e.g. 'f9520572-ac1d-11e6-b39e-3085a99791c9.f[5002:5185]'" - } - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/subset-1.0.json b/client/ayon_core/pipeline/schema/subset-1.0.json deleted file mode 100644 index a299a6d341..0000000000 --- a/client/ayon_core/pipeline/schema/subset-1.0.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:subset-1.0", - "description": "A container of instances", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "name", - "versions" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string" - }, - "name": { - "description": "Name of directory", - "type": "string" - }, - "versions": { - "type": "array", - "items": { - "$ref": "version.json" - } - } - }, - - "definitions": {} -} diff --git a/client/ayon_core/pipeline/schema/subset-2.0.json b/client/ayon_core/pipeline/schema/subset-2.0.json deleted file mode 100644 index db256ec7fb..0000000000 --- a/client/ayon_core/pipeline/schema/subset-2.0.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:subset-2.0", - "description": "A container of instances", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:subset-2.0"], - "example": "openpype:subset-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["subset"], - "example": "subset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "shot01" - }, - "data": { - "type": "object", - "description": "Document metadata", - "example": { - "frameStart": 1000, - "frameEnd": 1201 - } - } - } -} diff --git a/client/ayon_core/pipeline/schema/subset-3.0.json b/client/ayon_core/pipeline/schema/subset-3.0.json deleted file mode 100644 index 1a0db53c04..0000000000 --- a/client/ayon_core/pipeline/schema/subset-3.0.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:subset-3.0", - "description": "A container of instances", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:subset-3.0"], - "example": "openpype:subset-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["subset"], - "example": "subset" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Name of directory", - "type": "string", - "pattern": "^[a-zA-Z0-9_.]*$", - "example": "shot01" - }, - "data": { - "description": "Document metadata", - "type": "object", - "required": ["families"], - "properties": { - "families": { - "type": "array", - "items": {"type": "string"}, - "description": "One or more families associated with this subset" - } - }, - "example": { - "families" : [ - "avalon.camera" - ], - "frameStart": 1000, - "frameEnd": 1201 - } - } - } -} diff --git a/client/ayon_core/pipeline/schema/thumbnail-1.0.json b/client/ayon_core/pipeline/schema/thumbnail-1.0.json deleted file mode 100644 index 5bdf78a4b1..0000000000 --- a/client/ayon_core/pipeline/schema/thumbnail-1.0.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:thumbnail-1.0", - "description": "Entity with thumbnail data", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:thumbnail-1.0"], - "example": "openpype:thumbnail-1.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["thumbnail"], - "example": "thumbnail" - }, - "data": { - "description": "Thumbnail data", - "type": "object", - "example": { - "binary_data": "Binary({byte data of image})", - "template": "{thumbnail_root}/{project[name]}/{_id}{ext}}", - "template_data": { - "ext": ".jpg" - } - } - } - } -} diff --git a/client/ayon_core/pipeline/schema/version-1.0.json b/client/ayon_core/pipeline/schema/version-1.0.json deleted file mode 100644 index daa1997721..0000000000 --- a/client/ayon_core/pipeline/schema/version-1.0.json +++ /dev/null @@ -1,50 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:version-1.0", - "description": "An individual version", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "version", - "path", - "time", - "author", - "source", - "representations" - ], - - "properties": { - "schema": {"type": "string"}, - "representations": { - "type": "array", - "items": { - "$ref": "representation.json" - } - }, - "time": { - "description": "ISO formatted, file-system compatible time", - "type": "string" - }, - "author": { - "description": "User logged on to the machine at time of publish", - "type": "string" - }, - "version": { - "description": "Number of this version", - "type": "number" - }, - "path": { - "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", - "type": "string" - }, - "source": { - "description": "Original file from which this version was made.", - "type": "string" - } - } -} diff --git a/client/ayon_core/pipeline/schema/version-2.0.json b/client/ayon_core/pipeline/schema/version-2.0.json deleted file mode 100644 index 099e9be70a..0000000000 --- a/client/ayon_core/pipeline/schema/version-2.0.json +++ /dev/null @@ -1,92 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:version-2.0", - "description": "An individual version", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:version-2.0"], - "example": "openpype:version-2.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["version"], - "example": "version" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Number of version", - "type": "number", - "example": 12 - }, - "locations": { - "description": "Where on the planet this version can be found.", - "type": "array", - "items": {"type": "string"}, - "example": ["data.avalon.com"] - }, - "data": { - "description": "Document metadata", - "type": "object", - "required": ["families", "author", "source", "time"], - "properties": { - "time": { - "description": "ISO formatted, file-system compatible time", - "type": "string" - }, - "timeFormat": { - "description": "ISO format of time", - "type": "string" - }, - "author": { - "description": "User logged on to the machine at time of publish", - "type": "string" - }, - "version": { - "description": "Number of this version", - "type": "number" - }, - "path": { - "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", - "type": "string" - }, - "source": { - "description": "Original file from which this version was made.", - "type": "string" - }, - "families": { - "type": "array", - "items": {"type": "string"}, - "description": "One or more families associated with this version" - } - }, - "example": { - "source" : "{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma", - "author" : "marcus", - "families" : [ - "avalon.model" - ], - "time" : "20170510T090203Z" - } - } - } -} diff --git a/client/ayon_core/pipeline/schema/version-3.0.json b/client/ayon_core/pipeline/schema/version-3.0.json deleted file mode 100644 index 3e07fc4499..0000000000 --- a/client/ayon_core/pipeline/schema/version-3.0.json +++ /dev/null @@ -1,84 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:version-3.0", - "description": "An individual version", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "parent", - "name", - "data" - ], - - "properties": { - "schema": { - "description": "The schema associated with this document", - "type": "string", - "enum": ["openpype:version-3.0"], - "example": "openpype:version-3.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["version"], - "example": "version" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "name": { - "description": "Number of version", - "type": "number", - "example": 12 - }, - "locations": { - "description": "Where on the planet this version can be found.", - "type": "array", - "items": {"type": "string"}, - "example": ["data.avalon.com"] - }, - "data": { - "description": "Document metadata", - "type": "object", - "required": ["author", "source", "time"], - "properties": { - "time": { - "description": "ISO formatted, file-system compatible time", - "type": "string" - }, - "timeFormat": { - "description": "ISO format of time", - "type": "string" - }, - "author": { - "description": "User logged on to the machine at time of publish", - "type": "string" - }, - "version": { - "description": "Number of this version", - "type": "number" - }, - "path": { - "description": "Unformatted path, e.g. '{root}/assets/Bruce/publish/lookdevDefault/v001", - "type": "string" - }, - "source": { - "description": "Original file from which this version was made.", - "type": "string" - } - }, - "example": { - "source" : "{root}/f02_prod/assets/BubbleWitch/work/modeling/marcus/maya/scenes/model_v001.ma", - "author" : "marcus", - "time" : "20170510T090203Z" - } - } - } -} diff --git a/client/ayon_core/pipeline/schema/workfile-1.0.json b/client/ayon_core/pipeline/schema/workfile-1.0.json deleted file mode 100644 index 5f9600ef20..0000000000 --- a/client/ayon_core/pipeline/schema/workfile-1.0.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-04/schema#", - - "title": "openpype:workfile-1.0", - "description": "Workfile additional information.", - - "type": "object", - - "additionalProperties": true, - - "required": [ - "schema", - "type", - "filename", - "task_name", - "parent" - ], - - "properties": { - "schema": { - "description": "Schema identifier for payload", - "type": "string", - "enum": ["openpype:workfile-1.0"], - "example": "openpype:workfile-1.0" - }, - "type": { - "description": "The type of document", - "type": "string", - "enum": ["workfile"], - "example": "workfile" - }, - "parent": { - "description": "Unique identifier to parent document", - "example": "592c33475f8c1b064c4d1696" - }, - "filename": { - "description": "Workfile's filename", - "type": "string", - "example": "kuba_each_case_Alpaca_01_animation_v001.ma" - }, - "task_name": { - "description": "Task name", - "type": "string", - "example": "animation" - }, - "data": { - "description": "Document metadata", - "type": "object", - "example": {"key": "value"} - } - } -} diff --git a/client/ayon_core/pipeline/template_data.py b/client/ayon_core/pipeline/template_data.py index e9c57521d4..526c7d35c5 100644 --- a/client/ayon_core/pipeline/template_data.py +++ b/client/ayon_core/pipeline/template_data.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_project, get_asset_by_name +import ayon_api + from ayon_core.settings import get_studio_settings from ayon_core.lib.local_settings import get_ayon_username @@ -27,13 +28,12 @@ def get_general_template_data(settings=None): } -def get_project_template_data(project_doc=None, project_name=None): +def get_project_template_data(project_entity=None, project_name=None): """Extract data from project document that are used in templates. - Project document must have 'name' and (at this moment) optional - key 'data.code'. + Project document must have 'name' and 'code'. - One of 'project_name' or 'project_doc' must be passed. With prepared + One of 'project_name' or 'project_entity' must be passed. With prepared project document is function much faster because don't have to query. Output contains formatting keys: @@ -41,7 +41,7 @@ def get_project_template_data(project_doc=None, project_name=None): - 'project[code]' - Project code Args: - project_doc (Dict[str, Any]): Queried project document. + project_entity (Dict[str, Any]): Queried project entity. project_name (str): Name of project. Returns: @@ -49,12 +49,12 @@ def get_project_template_data(project_doc=None, project_name=None): """ if not project_name: - project_name = project_doc["name"] + project_name = project_entity["name"] - if not project_doc: - project_doc = get_project(project_name, fields=["data.code"]) + elif not project_entity: + project_entity = ayon_api.get_project(project_name, fields=["code"]) - project_code = project_doc.get("data", {}).get("code") + project_code = project_entity["code"] return { "project": { "name": project_name, @@ -63,86 +63,74 @@ def get_project_template_data(project_doc=None, project_name=None): } -def get_asset_template_data(asset_doc, project_name): - """Extract data from asset document that are used in templates. +def get_folder_template_data(folder_entity, project_name): + """Extract data from folder entity that are used in templates. Output dictionary contains keys: - - 'asset' - asset name - - 'hierarchy' - parent asset names joined with '/' - - 'parent' - direct parent name, project name used if is under project + - 'folder' - dictionary with 'name' key filled with folder name + - 'asset' - folder name + - 'hierarchy' - parent folder names joined with '/' + - 'parent' - direct parent name, project name used if is under + project Required document fields: - Asset: 'name', 'data.parents' + Folder: 'path' -> Plan to require: 'folderType' Args: - asset_doc (Dict[str, Any]): Queried asset document. - project_name (str): Is used for 'parent' key if asset doc does not have - any. + folder_entity (Dict[str, Any]): Folder entity. + project_name (str): Is used for 'parent' key if folder entity + does not have any. Returns: - Dict[str, str]: Data that are based on asset document and can be used + Dict[str, str]: Data that are based on folder entity and can be used in templates. """ - asset_parents = asset_doc["data"]["parents"] - hierarchy = "/".join(asset_parents) - if asset_parents: - parent_name = asset_parents[-1] + path = folder_entity["path"] + hierarchy_parts = path.split("/") + # Remove empty string from the beginning + hierarchy_parts.pop(0) + # Remove last part which is folder name + folder_name = hierarchy_parts.pop(-1) + hierarchy = "/".join(hierarchy_parts) + if hierarchy_parts: + parent_name = hierarchy_parts[-1] else: parent_name = project_name return { - "asset": asset_doc["name"], "folder": { - "name": asset_doc["name"] + "name": folder_name, }, + "asset": folder_name, "hierarchy": hierarchy, "parent": parent_name } -def get_task_type(asset_doc, task_name): - """Get task type based on asset document and task name. +def get_task_template_data(project_entity, task_entity): + """Prepare task template data. Required document fields: - Asset: 'data.tasks' + Project: 'tasksTypes' + Task: 'type' Args: - asset_doc (Dict[str, Any]): Queried asset document. - task_name (str): Task name which is under asset. - - Returns: - str: Task type name. - None: Task was not found on asset document. - """ - - asset_tasks_info = asset_doc["data"]["tasks"] - return asset_tasks_info.get(task_name, {}).get("type") - - -def get_task_template_data(project_doc, asset_doc, task_name): - """"Extract task specific data from project and asset documents. - - Required document fields: - Project: 'config.tasks' - Asset: 'data.tasks'. - - Args: - project_doc (Dict[str, Any]): Queried project document. - asset_doc (Dict[str, Any]): Queried asset document. - task_name (str): Name of task for which data should be returned. + project_entity (Dict[str, Any]): Project entity. + task_entity (Dict[str, Any]): Task entity. Returns: Dict[str, Dict[str, str]]: Template data - """ - project_task_types = project_doc["config"]["tasks"] - task_type = get_task_type(asset_doc, task_name) - task_code = project_task_types.get(task_type, {}).get("short_name") + """ + project_task_types = project_entity["taskTypes"] + task_types_by_name = {task["name"]: task for task in project_task_types} + task_type = task_entity["taskType"] + task_code = task_types_by_name.get(task_type, {}).get("shortName") return { "task": { - "name": task_name, + "name": task_entity["name"], "type": task_type, "short": task_code, } @@ -150,11 +138,11 @@ def get_task_template_data(project_doc, asset_doc, task_name): def get_template_data( - project_doc, - asset_doc=None, - task_name=None, + project_entity, + folder_entity=None, + task_entity=None, host_name=None, - settings=None + settings=None, ): """Prepare data for templates filling from entered documents and info. @@ -166,14 +154,15 @@ def get_template_data( and their values won't be added to template data if are not passed. Required document fields: - Project: 'name', 'data.code', 'config.tasks' - Asset: 'name', 'data.parents', 'data.tasks' + Project: 'name', 'code', 'taskTypes.name' + Folder: 'name', 'path' + Task: 'type' Args: - project_doc (Dict[str, Any]): Mongo document of project from MongoDB. - asset_doc (Dict[str, Any]): Mongo document of asset from MongoDB. - task_name (Union[str, None]): Task name under passed asset. - host_name (Union[str, None]): Used to fill '{app}' key. + project_entity (Dict[str, Any]): Project entity. + folder_entity (Optional[Dict[str, Any]]): Folder entity. + task_entity (Optional[Dict[str, Any]): Task entity. + 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). @@ -182,14 +171,14 @@ def get_template_data( """ template_data = get_general_template_data(settings) - template_data.update(get_project_template_data(project_doc)) - if asset_doc: - template_data.update(get_asset_template_data( - asset_doc, project_doc["name"] + template_data.update(get_project_template_data(project_entity)) + if folder_entity: + template_data.update(get_folder_template_data( + folder_entity, project_entity["name"] )) - if task_name: + if task_entity: template_data.update(get_task_template_data( - project_doc, asset_doc, task_name + project_entity, task_entity )) if host_name: @@ -200,7 +189,7 @@ def get_template_data( def get_template_data_with_names( project_name, - asset_name=None, + folder_path=None, task_name=None, host_name=None, settings=None @@ -211,30 +200,31 @@ def get_template_data_with_names( Only difference is that documents are queried. Args: - project_name (str): Project name for which template data are - calculated. - asset_name (Union[str, None]): Asset name for which template data are - calculated. - task_name (Union[str, None]): Task name under passed asset. - host_name (Union[str, None]):Used to fill '{app}' key. + project_name (str): Project name. + folder_path (Optional[str]): Folder path. + task_name (Optional[str]): Task name. + host_name (Optional[str]):Used to fill '{app}' key. because workdir template may contain `{app}` key. - settings (Union[Dict, None]): Prepared studio or project settings. + settings (Optional[Dict]): Prepared studio or project settings. They're queried if not passed. Returns: Dict[str, Any]: Data prepared for filling workdir template. """ - project_doc = get_project( - project_name, fields=["name", "data.code", "config.tasks"] - ) - asset_doc = None - if asset_name: - asset_doc = get_asset_by_name( + project_entity = ayon_api.get_project(project_name) + folder_entity = None + task_entity = None + if folder_path: + folder_entity = ayon_api.get_folder_by_path( project_name, - asset_name, - fields=["name", "data.parents", "data.tasks"] + folder_path, + fields={"id", "path", "folderType"} ) + if task_name and folder_entity: + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) return get_template_data( - project_doc, asset_doc, task_name, host_name, settings + project_entity, folder_entity, task_entity, host_name, settings ) diff --git a/client/ayon_core/pipeline/usdlib.py b/client/ayon_core/pipeline/usdlib.py index 2a5a317d72..dedcee6f99 100644 --- a/client/ayon_core/pipeline/usdlib.py +++ b/client/ayon_core/pipeline/usdlib.py @@ -2,14 +2,15 @@ import os import re import logging +import ayon_api try: from pxr import Usd, UsdGeom, Sdf, Kind except ImportError: # Allow to fall back on Multiverse 6.3.0+ pxr usd library from mvpxr import Usd, UsdGeom, Sdf, Kind -from ayon_core.client import get_project, get_asset_by_name from ayon_core.pipeline import Anatomy, get_current_project_name +from ayon_core.pipeline.template_data import get_template_data log = logging.getLogger(__name__) @@ -118,7 +119,7 @@ def create_shot(filepath, layers, create_layers=False): return filepath -def create_model(filename, asset, variant_subsets): +def create_model(filename, folder_path, variant_product_names): """Create a USD Model file. For each of the variation paths it will payload the path and set its @@ -127,22 +128,24 @@ def create_model(filename, asset, variant_subsets): """ project_name = get_current_project_name() - asset_doc = get_asset_by_name(project_name, asset) - assert asset_doc, "Asset not found: %s" % asset + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + assert folder_entity, "Folder not found: %s" % folder_path variants = [] - for subset in variant_subsets: + for product_name in variant_product_names: prefix = "usdModel" - if subset.startswith(prefix): + if product_name.startswith(prefix): # Strip off `usdModel_` - variant = subset[len(prefix):] + variant = product_name[len(prefix):] else: raise ValueError( - "Model subsets must start " "with usdModel: %s" % subset + "Model products must start with usdModel: %s" % product_name ) path = get_usd_master_path( - asset=asset_doc, subset=subset, representation="usd" + folder_entity=folder_entity, + product_name=product_name, + representation="usd" ) variants.append((variant, path)) @@ -169,33 +172,37 @@ def create_model(filename, asset, variant_subsets): stage.GetRootLayer().Save() -def create_shade(filename, asset, variant_subsets): +def create_shade(filename, folder_path, variant_product_names): """Create a master USD shade file for an asset. For each available model variation this should generate a reference - to a `usdShade_{modelVariant}` subset. + to a `usdShade_{modelVariant}` product. """ project_name = get_current_project_name() - asset_doc = get_asset_by_name(project_name, asset) - assert asset_doc, "Asset not found: %s" % asset + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + assert folder_entity, "Folder not found: %s" % folder_path variants = [] - for subset in variant_subsets: + for product_name in variant_product_names: prefix = "usdModel" - if subset.startswith(prefix): + if product_name.startswith(prefix): # Strip off `usdModel_` - variant = subset[len(prefix):] + variant = product_name[len(prefix):] else: raise ValueError( - "Model subsets must start " "with usdModel: %s" % subset + "Model products must start " "with usdModel: %s" % product_name ) - shade_subset = re.sub("^usdModel", "usdShade", subset) + shade_product_name = re.sub( + "^usdModel", "usdShade", product_name + ) path = get_usd_master_path( - asset=asset_doc, subset=shade_subset, representation="usd" + folder_entity=folder_entity, + product_name=shade_product_name, + representation="usd" ) variants.append((variant, path)) @@ -206,7 +213,7 @@ def create_shade(filename, asset, variant_subsets): stage.GetRootLayer().Save() -def create_shade_variation(filename, asset, model_variant, shade_variants): +def create_shade_variation(filename, folder_path, model_variant, shade_variants): """Create the master Shade file for a specific model variant. This should reference all shade variants for the specific model variant. @@ -214,16 +221,18 @@ def create_shade_variation(filename, asset, model_variant, shade_variants): """ project_name = get_current_project_name() - asset_doc = get_asset_by_name(project_name, asset) - assert asset_doc, "Asset not found: %s" % asset + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + assert folder_entity, "Folder not found: %s" % folder_path variants = [] for variant in shade_variants: - subset = "usdShade_{model}_{shade}".format( + product_name = "usdShade_{model}_{shade}".format( model=model_variant, shade=variant ) path = get_usd_master_path( - asset=asset_doc, subset=subset, representation="usd" + folder_entity=folder_entity, + product_name=product_name, + representation="usd" ) variants.append((variant, path)) @@ -306,55 +315,46 @@ def _create_variants_file( return stage -def get_usd_master_path(asset, subset, representation): - """Get the filepath for a .usd file of a subset. +def get_usd_master_path(folder_entity, product_name, representation): + """Get the filepath for a .usd file of a product. This will return the path to an unversioned master file generated by `usd_master_file.py`. + Args: + folder_entity (Union[str, dict]): Folder entity. + product_name (str): Product name. + representation (str): Representation name. """ project_name = get_current_project_name() - anatomy = Anatomy(project_name) - project_doc = get_project( - project_name, - fields=["name", "data.code"] - ) + project_entity = ayon_api.get_project(project_name) + anatomy = Anatomy(project_name, project_entity=project_entity) - if isinstance(asset, dict) and "name" in asset: - # Allow explicitly passing asset document - asset_doc = asset - else: - asset_doc = get_asset_by_name(project_name, asset, fields=["name"]) + template_data = get_template_data(project_entity, folder_entity) + template_data.update({ + "product": { + "name": product_name + }, + "subset": product_name, + "representation": representation, + "version": 0, # stub version zero + }) template_obj = anatomy.templates_obj["publish"]["path"] - path = template_obj.format_strict( - { - "project": { - "name": project_name, - "code": project_doc.get("data", {}).get("code") - }, - "folder": { - "name": asset_doc["name"], - }, - "asset": asset_doc["name"], - "subset": subset, - "representation": representation, - "version": 0, # stub version zero - } - ) + path = template_obj.format_strict(template_data) # Remove the version folder - subset_folder = os.path.dirname(os.path.dirname(path)) - master_folder = os.path.join(subset_folder, "master") - fname = "{0}.{1}".format(subset, representation) + product_folder = os.path.dirname(os.path.dirname(path)) + master_folder = os.path.join(product_folder, "master") + fname = "{0}.{1}".format(product_name, representation) return os.path.join(master_folder, fname).replace("\\", "/") def parse_avalon_uri(uri): - # URI Pattern: avalon://{asset}/{subset}.{ext} - pattern = r"avalon://(?P[^/.]*)/(?P[^/]*)\.(?P.*)" + # URI Pattern: avalon://{folder}/{product}.{ext} + pattern = r"avalon://(?P[^/.]*)/(?P[^/]*)\.(?P.*)" if uri.startswith("avalon://"): match = re.match(pattern, uri) if match: diff --git a/client/ayon_core/pipeline/workfile/build_workfile.py b/client/ayon_core/pipeline/workfile/build_workfile.py index 34d8ef0c8f..6d7ea2c7ad 100644 --- a/client/ayon_core/pipeline/workfile/build_workfile.py +++ b/client/ayon_core/pipeline/workfile/build_workfile.py @@ -13,13 +13,8 @@ import re import collections import json -from ayon_core.client import ( - get_asset_by_name, - get_subsets, - get_last_versions, - get_representations, - get_linked_assets, -) +import ayon_api + from ayon_core.settings import get_project_settings from ayon_core.lib import ( filter_profiles, @@ -48,17 +43,11 @@ class BuildWorkfile: return self._log @staticmethod - def map_products_by_type(subset_docs): + def map_products_by_type(product_entities): products_by_type = collections.defaultdict(list) - for subset_doc in subset_docs: - product_type = subset_doc["data"].get("family") - if not product_type: - families = subset_doc["data"].get("families") - if not families: - continue - product_type = families[0] - - products_by_type[product_type].append(subset_doc) + for product_entity in product_entities: + product_type = product_entity["productType"] + products_by_type[product_type].append(product_entity) return products_by_type def process(self): @@ -76,21 +65,21 @@ class BuildWorkfile: def build_workfile(self): """Prepares and load containers into workfile. - Loads latest versions of current and linked assets to workfile by logic - stored in Workfile profiles from presets. Profiles are set by host, - filtered by current task name and used by families. + Loads latest versions of current and linked folders to workfile by + logic stored in Workfile profiles from presets. Profiles are set + by host, filtered by current task name and used by families. Each product type can specify representation names and loaders for representations and first available and successful loaded representation is returned as container. - At the end you'll get list of loaded containers per each asset. + At the end you'll get list of loaded containers per each folder. loaded_containers [{ - "asset_doc": , + "folder_entity": , "containers": [, , ...] }, { - "asset_doc": , + "folder_entity": , "containers": [, ...] }, { ... @@ -100,22 +89,21 @@ class BuildWorkfile: List[Dict[str, Any]]: Loaded containers during build. """ - from ayon_core.pipeline.context_tools import ( - get_current_project_name, - get_current_asset_name, - get_current_task_name, - ) + from ayon_core.pipeline.context_tools import get_current_context loaded_containers = [] - # Get current asset name and entity - project_name = get_current_project_name() - current_folder_path = get_current_asset_name() - current_asset_doc = get_asset_by_name( + # Get current folder and task entities + context = get_current_context() + project_name = context["project_name"] + current_folder_path = context["folder_path"] + current_task_name = context["task_name"] + + current_folder_entity = ayon_api.get_folder_by_path( project_name, current_folder_path ) - # Skip if asset was not found - if not current_asset_doc: + # Skip if folder was not found + if not current_folder_entity: print("Folder entity `{}` was not found".format( current_folder_path )) @@ -138,12 +126,9 @@ class BuildWorkfile: self.log.warning("There are no registered loaders.") return loaded_containers - # Get current task name - current_task_name = get_current_task_name() - # Load workfile presets for task self.build_presets = self.get_build_presets( - current_task_name, current_asset_doc + current_task_name, current_folder_entity["id"] ) # Skip if there are any presets for task @@ -180,45 +165,53 @@ class BuildWorkfile: "loading preset for it's linked folders." ).format(current_task_name)) - # Prepare assets to process by workfile presets - asset_docs = [] + # Prepare folders to process by workfile presets + folder_entities = [] current_folder_id = None if current_context_profiles: - # Add current asset entity if preset has current context set - asset_docs.append(current_asset_doc) - current_folder_id = current_asset_doc["_id"] + # Add current folder entity if preset has current context set + folder_entities.append(current_folder_entity) + current_folder_id = current_folder_entity["id"] if link_context_profiles: - # Find and append linked assets if preset has set linked mapping - link_assets = get_linked_assets(project_name, current_asset_doc) - if link_assets: - asset_docs.extend(link_assets) + # Find and append linked folders if preset has set linked mapping + linked_folder_entities = self._get_linked_folder_entities( + project_name, current_folder_entity["id"] + ) + if linked_folder_entities: + folder_entities.extend(linked_folder_entities) - # Skip if there are no assets. This can happen if only linked mapping - # is set and there are no links for his asset. - if not asset_docs: + # Skip if there are no folders. This can happen if only linked mapping + # is set and there are no links for his folder. + if not folder_entities: self.log.warning( - "Asset does not have linked assets. Nothing to process." + "Folder does not have linked folders. Nothing to process." ) return loaded_containers - # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(asset_docs) + # Prepare entities from database for folders + prepared_entities = self._collect_last_version_repres( + folder_entities + ) # Load containers by prepared entities and presets - # - Current asset containers + # - Current folder containers if current_folder_id and current_folder_id in prepared_entities: current_context_data = prepared_entities.pop(current_folder_id) - loaded_data = self.load_containers_by_asset_data( - current_context_data, current_context_profiles, loaders_by_name + loaded_data = self.load_containers_by_folder_data( + current_context_data, + current_context_profiles, + loaders_by_name ) if loaded_data: loaded_containers.append(loaded_data) # - Linked assets container - for linked_asset_data in prepared_entities.values(): - loaded_data = self.load_containers_by_asset_data( - linked_asset_data, link_context_profiles, loaders_by_name + for linked_folder_data in prepared_entities.values(): + loaded_data = self.load_containers_by_folder_data( + linked_folder_data, + link_context_profiles, + loaders_by_name ) if loaded_data: loaded_containers.append(loaded_data) @@ -226,7 +219,7 @@ class BuildWorkfile: # Return list of loaded containers return loaded_containers - def get_build_presets(self, task_name, asset_doc): + def get_build_presets(self, task_name, folder_id): """ Returns presets to build workfile for task name. Presets are loaded for current project received by @@ -235,6 +228,7 @@ class BuildWorkfile: Args: task_name (str): Task name used for filtering build presets. + folder_id (str): Folder id. Returns: Dict[str, Any]: preset per entered task name @@ -245,10 +239,9 @@ class BuildWorkfile: get_current_project_name, ) + project_name = get_current_project_name() host_name = get_current_host_name() - project_settings = get_project_settings( - get_current_project_name() - ) + project_settings = get_project_settings(project_name) host_settings = project_settings.get(host_name) or {} # Get presets for host @@ -261,13 +254,15 @@ class BuildWorkfile: if not builder_profiles: return None - task_type = ( - asset_doc - .get("data", {}) - .get("tasks", {}) - .get(task_name, {}) - .get("type") + task_entity = ayon_api.get_task_by_name( + project_name, + folder_id, + task_name, ) + task_type = None + if task_entity: + task_type = task_entity["taskType"] + filter_data = { "task_types": task_type, "tasks": task_name @@ -349,7 +344,32 @@ class BuildWorkfile: return valid_profiles - def _prepare_profile_for_products(self, subset_docs, profiles): + def _get_linked_folder_entities(self, project_name, folder_id): + """Get linked folder entities for entered folder. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + + Returns: + list[dict[str, Any]]: Linked folder entities. + + """ + links = ayon_api.get_folder_links( + project_name, folder_id, link_direction="in" + ) + linked_folder_ids = { + link["entityId"] + for link in links + if link["entityType"] == "folder" + } + if not linked_folder_ids: + return [] + return list(ayon_api.get_folders( + project_name, folder_ids=linked_folder_ids + )) + + def _prepare_profile_for_products(self, product_entities, profiles): """Select profile for each product by it's data. Profiles are filtered for each product individually. @@ -360,7 +380,7 @@ class BuildWorkfile: matching profile. Args: - subset_docs (List[Dict[str, Any]]): Subset documents. + product_entities (List[Dict[str, Any]]): product entities. profiles (List[Dict[str, Any]]): Build profiles. Returns: @@ -368,10 +388,10 @@ class BuildWorkfile: """ # Prepare products - products_by_type = self.map_products_by_type(subset_docs) + products_by_type = self.map_products_by_type(product_entities) profiles_by_product_id = {} - for product_type, subset_docs in products_by_type.items(): + for product_type, product_entities in products_by_type.items(): product_type_low = product_type.lower() for profile in profiles: # Skip profile if does not contain product type @@ -387,46 +407,46 @@ class BuildWorkfile: profile_regexes = _profile_regexes # TODO prepare regex compilation - for subset_doc in subset_docs: + for product_entity in product_entities: # Verify regex filtering (optional) if profile_regexes: valid = False for pattern in profile_regexes: - if re.match(pattern, subset_doc["name"]): + if re.match(pattern, product_entity["name"]): valid = True break if not valid: continue - profiles_by_product_id[subset_doc["_id"]] = profile + profiles_by_product_id[product_entity["id"]] = profile # break profiles loop on finding the first matching profile break return profiles_by_product_id - def load_containers_by_asset_data( - self, asset_doc_data, build_profiles, loaders_by_name + def load_containers_by_folder_data( + self, linked_folder_data, build_profiles, loaders_by_name ): - """Load containers for entered asset entity by Build profiles. + """Load containers for entered folder entity by Build profiles. Args: - asset_doc_data (Dict[str, Any]): Prepared data with products, - last versions and representations for specific asset. + linked_folder_data (Dict[str, Any]): Prepared data with products, + last versions and representations for specific folder. build_profiles (Dict[str, Any]): Build profiles. loaders_by_name (Dict[str, LoaderPlugin]): Available loaders per name. Returns: - Dict[str, Any]: Output contains asset document + Dict[str, Any]: Output contains folder entity and loaded containers. """ # Make sure all data are not empty - if not asset_doc_data or not build_profiles or not loaders_by_name: + if not linked_folder_data or not build_profiles or not loaders_by_name: return - asset_doc = asset_doc_data["asset_doc"] + folder_entity = linked_folder_data["folder_entity"] valid_profiles = self._filter_build_profiles( build_profiles, loaders_by_name @@ -442,20 +462,20 @@ class BuildWorkfile: products_by_id = {} version_by_product_id = {} repres_by_version_id = {} - for product_id, in_data in asset_doc_data["subsets"].items(): - subset_doc = in_data["subset_doc"] - products_by_id[subset_doc["_id"]] = subset_doc + for product_id, in_data in linked_folder_data["products"].items(): + product_entity = in_data["product_entity"] + products_by_id[product_entity["id"]] = product_entity version_data = in_data["version"] - version_doc = version_data["version_doc"] - version_by_product_id[product_id] = version_doc - repres_by_version_id[version_doc["_id"]] = ( + version_entity = version_data["version_entity"] + version_by_product_id[product_id] = version_entity + repres_by_version_id[version_entity["id"]] = ( version_data["repres"] ) if not products_by_id: - self.log.warning("There are not products for folder {0}".format( - asset_doc["name"] + self.log.warning("There are not products for folder {}".format( + folder_entity["path"] )) return @@ -470,8 +490,8 @@ class BuildWorkfile: for product_id, profile in profiles_by_product_id.items(): profile_repre_names = profile["repre_names_lowered"] - version_doc = version_by_product_id[product_id] - version_id = version_doc["_id"] + version_entity = version_by_product_id[product_id] + version_id = version_entity["id"] repres = repres_by_version_id[version_id] for repre in repres: repre_name_low = repre["name"].lower() @@ -480,12 +500,12 @@ class BuildWorkfile: # DEBUG message msg = "Valid representations for Folder: `{}`".format( - asset_doc["name"] + folder_entity["path"] ) for product_id, repres in valid_repres_by_product_id.items(): - subset_doc = products_by_id[product_id] + product_entity = products_by_id[product_id] msg += "\n# Product Name/ID: `{}`/{}".format( - subset_doc["name"], product_id + product_entity["name"], product_id ) for repre in repres: msg += "\n## Repre name: `{}`".format(repre["name"]) @@ -498,7 +518,7 @@ class BuildWorkfile: ) return { - "asset_doc": asset_doc, + "folder_entity": folder_entity, "containers": containers } @@ -514,13 +534,13 @@ class BuildWorkfile: If product has representation matching representation name each loader is tried to load it until any is successful. If none of them was successful then next representation name is tried. - Subset process loop ends when any representation is loaded or + Product process loop ends when any representation is loaded or all matching representations were already tried. Args: repres_by_product_id (Dict[str, Dict[str, Any]]): Available representations mapped by their parent (product) id. - products_by_id (Dict[str, Dict[str, Any]]): Subset documents + products_by_id (Dict[str, Dict[str, Any]]): Product entities mapped by their id. profiles_by_product_id (Dict[str, Dict[str, Any]]): Build profiles mapped by product id. @@ -539,9 +559,9 @@ class BuildWorkfile: product_ids_ordered = [] for preset in build_presets: for product_type in preset["product_types"]: - for product_id, subset_doc in products_by_id.items(): + for product_id, product_entity in products_by_id.items(): # TODO 'families' is not available on product - families = subset_doc["data"].get("families") or [] + families = product_entity["data"].get("families") or [] if product_type not in families: continue @@ -596,7 +616,7 @@ class BuildWorkfile: try: container = load_container( loader, - repre["_id"], + repre["id"], name=product_name ) loaded_containers.append(container) @@ -628,11 +648,11 @@ class BuildWorkfile: return loaded_containers - def _collect_last_version_repres(self, asset_docs): - """Collect products, versions and representations for asset_entities. + def _collect_last_version_repres(self, folder_entities): + """Collect products, versions and representations for folder_entities. Args: - asset_docs (List[Dict[str, Any]]): Asset entities for which + folder_entities (List[Dict[str, Any]]): Folder entities for which want to find data. Returns: @@ -641,13 +661,13 @@ class BuildWorkfile: Example output: ``` { - {Asset ID}: { - "asset_doc": , - "subsets": { - {Subset ID}: { - "subset_doc": , + : { + "folder_entity": , + "products": { + : { + "product_entity": , "version": { - "version_doc": , + "version_entity": , "repres": [ , , ... ] @@ -658,68 +678,73 @@ class BuildWorkfile: }, ... } - output[folder_id]["subsets"][product_id]["version"]["repres"] + output[folder_id]["products"][product_id]["version"]["repres"] ``` """ from ayon_core.pipeline.context_tools import get_current_project_name output = {} - if not asset_docs: + if not folder_entities: return output - asset_docs_by_ids = { - asset_doc["_id"]: asset_doc - for asset_doc in asset_docs + folder_entities_by_id = { + folder_entity["id"]: folder_entity + for folder_entity in folder_entities } project_name = get_current_project_name() - subset_docs = list(get_subsets( - project_name, asset_ids=asset_docs_by_ids.keys() + product_entities = list(ayon_api.get_products( + project_name, folder_ids=folder_entities_by_id.keys() )) - subset_docs_by_id = { - subset_doc["_id"]: subset_doc - for subset_doc in subset_docs + product_entities_by_id = { + product_entity["id"]: product_entity + for product_entity in product_entities } - last_version_by_product_id = get_last_versions( - project_name, subset_docs_by_id.keys() + last_version_by_product_id = ayon_api.get_last_versions( + project_name, product_entities_by_id.keys() ) - last_version_docs_by_id = { - version["_id"]: version - for version in last_version_by_product_id.values() + last_version_entities_by_id = { + version_entity["id"]: version_entity + for version_entity in last_version_by_product_id.values() } - repre_docs = get_representations( - project_name, version_ids=last_version_docs_by_id.keys() + repre_entities = ayon_api.get_representations( + project_name, version_ids=last_version_entities_by_id.keys() ) - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_doc = last_version_docs_by_id[version_id] + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + version_entity = last_version_entities_by_id[version_id] - product_id = version_doc["parent"] - subset_doc = subset_docs_by_id[product_id] + product_id = version_entity["productId"] + product_entity = product_entities_by_id[product_id] - folder_id = subset_doc["parent"] - asset_doc = asset_docs_by_ids[folder_id] + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] if folder_id not in output: output[folder_id] = { - "asset_doc": asset_doc, - "subsets": {} + "folder_entity": folder_entity, + "products": {} } - if product_id not in output[folder_id]["subsets"]: - output[folder_id]["subsets"][product_id] = { - "subset_doc": subset_doc, + if product_id not in output[folder_id]["products"]: + output[folder_id]["products"][product_id] = { + "product_entity": product_entity, "version": { - "version_doc": version_doc, + "version_entity": version_entity, "repres": [] } } - output[folder_id]["subsets"][product_id]["version"]["repres"].append( - repre_doc - ) + ( + output + [folder_id] + ["products"] + [product_id] + ["version"] + ["repres"] + ).append(repre_entity) return output diff --git a/client/ayon_core/pipeline/workfile/path_resolving.py b/client/ayon_core/pipeline/workfile/path_resolving.py index 7718e32317..5d36f432ad 100644 --- a/client/ayon_core/pipeline/workfile/path_resolving.py +++ b/client/ayon_core/pipeline/workfile/path_resolving.py @@ -3,7 +3,8 @@ import re import copy import platform -from ayon_core.client import get_project, get_asset_by_name +import ayon_api + from ayon_core.settings import get_project_settings from ayon_core.lib import ( filter_profiles, @@ -15,7 +16,11 @@ from ayon_core.pipeline.template_data import get_template_data def get_workfile_template_key_from_context( - asset_name, task_name, host_name, project_name, project_settings=None + project_name, + folder_path, + task_name, + host_name, + project_settings=None ): """Helper function to get template key for workfile template. @@ -23,41 +28,39 @@ def get_workfile_template_key_from_context( context". Args: - asset_name(str): Name of asset document. - task_name(str): Task name for which is template key retrieved. - Must be available on asset document under `data.tasks`. - host_name(str): Name of host implementation for which is workfile - used. - project_name(str): Project name where asset and task is. - project_settings(Dict[str, Any]): Project settings for passed + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. + host_name (str): Host name. + project_settings (Dict[str, Any]): Project settings for passed 'project_name'. Not required at all but makes function faster. """ - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["data.tasks"] + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} ) - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + task_type = task_entity.get("type") return get_workfile_template_key( - task_type, host_name, project_name, project_settings + project_name, task_type, host_name, project_settings ) def get_workfile_template_key( - task_type, host_name, project_name, project_settings=None + project_name, task_type, host_name, project_settings=None ): """Workfile template key which should be used to get workfile template. Function is using profiles from project settings to return right template - for passet task type and host name. + for passed task type and host name. Args: - task_type(str): Name of task type. - host_name(str): Name of host implementation (e.g. "maya", "nuke", ...) - project_name(str): Name of project in which context should look for - settings. + project_name(str): Project name. + task_type(str): Task type. + host_name(str): Host name (e.g. "maya", "nuke", ...) project_settings(Dict[str, Any]): Prepared project settings for project name. Optional to make processing faster. """ @@ -126,9 +129,9 @@ def get_workdir_with_workdir_data( if not template_key: template_key = get_workfile_template_key( + workdir_data["project"]["name"], workdir_data["task"]["type"], workdir_data["app"], - workdir_data["project"]["name"], project_settings ) @@ -141,9 +144,9 @@ def get_workdir_with_workdir_data( def get_workdir( - project_doc, - asset_doc, - task_name, + project_entity, + folder_entity, + task_entity, host_name, anatomy=None, template_key=None, @@ -152,14 +155,14 @@ def get_workdir( """Fill workdir path from entered data and project's anatomy. Args: - project_doc (Dict[str, Any]): Mongo document of project from MongoDB. - asset_doc (Dict[str, Any]): Mongo document of asset from MongoDB. - task_name (str): Task name for which are workdir data preapred. + project_entity (Dict[str, Any]): Project entity. + folder_entity (Dict[str, Any]): Folder entity. + task_entity (dict[str, Any]): Task entity. host_name (str): Host which is used to workdir. This is required because workdir template may contain `{app}` key. In `Session` is stored under `AYON_HOST_NAME` key. anatomy (Anatomy): Optional argument. Anatomy object is created using - project name from `project_doc`. It is preferred to pass this + project name from `project_entity`. It is preferred to pass this argument as initialization of a new Anatomy object may be time consuming. template_key (str): Key of work templates in anatomy templates. Default @@ -173,10 +176,15 @@ def get_workdir( """ if not anatomy: - anatomy = Anatomy(project_doc["name"]) + anatomy = Anatomy( + project_entity["name"], project_entity=project_entity + ) workdir_data = get_template_data( - project_doc, asset_doc, task_name, host_name + project_entity, + folder_entity, + task_entity, + host_name, ) # Output is TemplateResult object which contain useful data return get_workdir_with_workdir_data( @@ -336,9 +344,9 @@ def get_last_workfile( def get_custom_workfile_template( - project_doc, - asset_doc, - task_name, + project_entity, + folder_entity, + task_entity, host_name, anatomy=None, project_settings=None @@ -356,13 +364,13 @@ def get_custom_workfile_template( points to a file which is copied as first workfile - It is expected that passed argument are already queried documents of - project and asset as parents of processing task name. + It is expected that passed argument are already queried entities of + project and folder as parents of processing task name. Args: - project_doc (Dict[str, Any]): Project document from MongoDB. - asset_doc (Dict[str, Any]): Asset document from MongoDB. - task_name (str): Name of task for which templates are filtered. + project_entity (Dict[str, Any]): Project entity. + folder_entity (Dict[str, Any]): Folder entity. + task_entity (Dict[str, Any]): Task entity. host_name (str): Name of host. anatomy (Anatomy): Optionally passed anatomy object for passed project name. @@ -376,7 +384,7 @@ def get_custom_workfile_template( log = Logger.get_logger("CustomWorkfileResolve") - project_name = project_doc["name"] + project_name = project_entity["name"] if project_settings is None: project_settings = get_project_settings(project_name) @@ -411,9 +419,9 @@ def get_custom_workfile_template( if anatomy is None: anatomy = Anatomy(project_name) - # get project, asset, task anatomy context data + # get project, folder, task anatomy context data anatomy_context_data = get_template_data( - project_doc, asset_doc, task_name, host_name + project_entity, folder_entity, task_entity, host_name ) # add root dict anatomy_context_data["root"] = anatomy.roots @@ -444,7 +452,7 @@ def get_custom_workfile_template( def get_custom_workfile_template_by_string_context( project_name, - asset_name, + folder_path, task_name, host_name, anatomy=None, @@ -452,30 +460,38 @@ def get_custom_workfile_template_by_string_context( ): """Filter and fill workfile template profiles by passed context. - Passed context are string representations of project, asset and task. - Function will query documents of project and asset to be able use + Passed context are string representations of project, folder and task. + Function will query documents of project and folder to be able to use `get_custom_workfile_template` for rest of logic. Args: - project_name(str): Project name. - asset_name(str): Asset name. - task_name(str): Task name. + project_name (str): Project name. + folder_path (str): Folder path. + task_name (str): Task name. host_name (str): Name of host. - anatomy(Anatomy): Optionally prepared anatomy object for passed + anatomy (Anatomy): Optionally prepared anatomy object for passed project. - project_settings(Dict[str, Any]): Preloaded project settings. + project_settings (Dict[str, Any]): Preloaded project settings. Returns: - str: Path to template or None if none of profiles match current - context. (Existence of formatted path is not validated.) - None: If no profile is matching context. + Union[str, None]: Path to template or None if none of profiles match + current context. (Existence of formatted path is not validated.) + """ - project_doc = get_project(project_name) - asset_doc = get_asset_by_name(project_name, asset_name) + project_entity = ayon_api.get_project(project_name) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) return get_custom_workfile_template( - project_doc, asset_doc, task_name, host_name, anatomy, project_settings + project_entity, + folder_entity, + task_entity, + host_name, + anatomy, + project_settings ) diff --git a/client/ayon_core/pipeline/workfile/workfile_template_builder.py b/client/ayon_core/pipeline/workfile/workfile_template_builder.py index c889e0cafb..e8b5268a6d 100644 --- a/client/ayon_core/pipeline/workfile/workfile_template_builder.py +++ b/client/ayon_core/pipeline/workfile/workfile_template_builder.py @@ -18,15 +18,16 @@ import copy from abc import ABCMeta, abstractmethod import six -from ayon_api import get_products, get_last_versions -from ayon_api.graphql_queries import folders_graphql_query - -from ayon_core.client import ( - get_asset_by_name, - get_linked_assets, +from ayon_api import ( + get_folders, + get_folder_by_path, + get_folder_links, + get_task_by_name, + get_products, + get_last_versions, get_representations, - get_ayon_server_api_connection, ) + from ayon_core.settings import get_project_settings from ayon_core.host import IWorkfileHost, HostBase from ayon_core.lib import ( @@ -39,7 +40,7 @@ from ayon_core.lib.attribute_definitions import get_attributes_keys from ayon_core.pipeline import Anatomy from ayon_core.pipeline.load import ( get_loaders_by_name, - get_contexts_for_repre_docs, + get_representation_contexts, load_with_repre_context, ) @@ -48,6 +49,8 @@ from ayon_core.pipeline.create import ( CreateContext, ) +_NOT_SET = object() + class TemplateNotFound(Exception): """Exception raised when template does not exist.""" @@ -117,9 +120,9 @@ class AbstractTemplateBuilder(object): self._project_settings = None - self._current_asset_doc = None - self._linked_asset_docs = None - self._task_type = None + self._current_folder_entity = _NOT_SET + self._current_task_entity = _NOT_SET + self._linked_folder_entities = _NOT_SET @property def project_name(self): @@ -128,9 +131,9 @@ class AbstractTemplateBuilder(object): return os.getenv("AYON_PROJECT_NAME") @property - def current_asset_name(self): + def current_folder_path(self): if isinstance(self._host, HostBase): - return self._host.get_current_asset_name() + return self._host.get_current_folder_path() return os.getenv("AYON_FOLDER_PATH") @property @@ -144,7 +147,7 @@ class AbstractTemplateBuilder(object): return self._host.get_current_context() return { "project_name": self.project_name, - "folder_path": self.current_asset_name, + "folder_path": self.current_folder_path, "task_name": self.current_task_name } @@ -155,33 +158,39 @@ class AbstractTemplateBuilder(object): return self._project_settings @property - def current_asset_doc(self): - if self._current_asset_doc is None: - self._current_asset_doc = get_asset_by_name( - self.project_name, self.current_asset_name + def current_folder_entity(self): + if self._current_folder_entity is _NOT_SET: + self._current_folder_entity = get_folder_by_path( + self.project_name, self.current_folder_path ) - return self._current_asset_doc + return self._current_folder_entity @property - def linked_asset_docs(self): - if self._linked_asset_docs is None: - self._linked_asset_docs = get_linked_assets( - self.project_name, self.current_asset_doc - ) - return self._linked_asset_docs + def linked_folder_entities(self): + if self._linked_folder_entities is _NOT_SET: + self._linked_folder_entities = self._get_linked_folder_entities() + return self._linked_folder_entities + + @property + def current_task_entity(self): + if self._current_task_entity is _NOT_SET: + task_entity = None + folder_entity = self.current_folder_entity + if folder_entity: + task_entity = get_task_by_name( + self.project_name, + folder_entity["id"], + self.current_task_name + ) + self._current_task_entity = task_entity + return self._current_task_entity @property def current_task_type(self): - asset_doc = self.current_asset_doc - if not asset_doc: - return None - return ( - asset_doc - .get("data", {}) - .get("tasks", {}) - .get(self.current_task_name, {}) - .get("type") - ) + task_entity = self.current_task_entity + if task_entity: + return task_entity["taskType"] + return None @property def create_context(self): @@ -242,9 +251,9 @@ class AbstractTemplateBuilder(object): self._loaders_by_name = None self._creators_by_name = None - self._current_asset_doc = None - self._linked_asset_docs = None - self._task_type = None + self._current_folder_entity = _NOT_SET + self._current_task_entity = _NOT_SET + self._linked_folder_entities = _NOT_SET self._project_settings = None @@ -256,6 +265,22 @@ class AbstractTemplateBuilder(object): self._loaders_by_name = get_loaders_by_name() return self._loaders_by_name + def _get_linked_folder_entities(self): + project_name = self.project_name + folder_entity = self.current_folder_entity + if not folder_entity: + return [] + links = get_folder_links( + project_name, folder_entity["id"], link_direction="in" + ) + linked_folder_ids = { + link["entityId"] + for link in links + if link["entityType"] == "folder" + } + + return list(get_folders(project_name, folder_ids=linked_folder_ids)) + def _collect_legacy_creators(self): creators_by_name = {} for creator in discover_legacy_creator_plugins(): @@ -305,8 +330,8 @@ class AbstractTemplateBuilder(object): different key or if the key is not already used for something else. Key should be self explanatory to content. - - wrong: 'asset' - - good: 'asset_name' + - wrong: 'folder' + - good: 'folder_name' Args: key (str): Key under which is key stored. @@ -351,8 +376,8 @@ class AbstractTemplateBuilder(object): different key or if the key is not already used for something else. Key should be self explanatory to content. - - wrong: 'asset' - - good: 'asset_name' + - wrong: 'folder' + - good: 'folder_path' Args: key (str): Key under which is key stored. @@ -371,8 +396,8 @@ class AbstractTemplateBuilder(object): different key or if the key is not already used for something else. Key should be self explanatory to content. - - wrong: 'asset' - - good: 'asset_name' + - wrong: 'folder' + - good: 'folder_path' Args: key (str): Key under which is key stored. @@ -1021,8 +1046,8 @@ class PlaceholderPlugin(object): Using shared data from builder but stored under plugin identifier. Key should be self explanatory to content. - - wrong: 'asset' - - good: 'asset_name' + - wrong: 'folder' + - good: 'folder_path' Args: key (str): Key under which is key stored. @@ -1061,8 +1086,8 @@ class PlaceholderPlugin(object): Using shared data from builder but stored under plugin identifier. Key should be self explanatory to content. - - wrong: 'asset' - - good: 'asset_name' + - wrong: 'folder' + - good: 'folder_path' Shared populate data are cleaned up during populate while loop. @@ -1323,7 +1348,7 @@ class PlaceholderLoadMixin(object): items=loader_items, tooltip=( "Loader" - "\nDefines what OpenPype loader will be used to" + "\nDefines what AYON loader will be used to" " load assets." "\nUseable loader depends on current host's loader list." "\nField is case sensitive." @@ -1408,50 +1433,6 @@ class PlaceholderLoadMixin(object): return {} - def _query_by_folder_regex(self, project_name, folder_regex): - """Query folders by folder path regex. - - WARNING: - This method will be removed once the same functionality is - available in ayon-python-api. - - Args: - project_name (str): Project name. - folder_regex (str): Regex for folder path. - - Returns: - list[str]: List of folder paths. - """ - - query = folders_graphql_query({"id"}) - - folders_field = None - for child in query._children: - if child.path != "project": - continue - - for project_child in child._children: - if project_child.path == "project/folders": - folders_field = project_child - break - if folders_field: - break - - if "folderPathRegex" not in query._variables: - folder_path_regex_var = query.add_variable( - "folderPathRegex", "String!" - ) - folders_field.set_filter("pathEx", folder_path_regex_var) - - query.set_variable_value("projectName", project_name) - if folder_regex: - query.set_variable_value("folderPathRegex", folder_regex) - - api = get_ayon_server_api_connection() - for parsed_data in query.continuous_query(api): - for folder in parsed_data["project"]["folders"]: - yield folder["id"] - def _get_representations(self, placeholder): """Prepared query of representations based on load options. @@ -1461,7 +1442,7 @@ class PlaceholderLoadMixin(object): Note: This returns all representation documents from all versions of matching product. To filter for last version use - '_reduce_last_version_repre_docs'. + '_reduce_last_version_repre_entities'. Args: placeholder (PlaceholderItem): Item which should be populated. @@ -1480,7 +1461,7 @@ class PlaceholderLoadMixin(object): return [] project_name = self.builder.project_name - current_asset_doc = self.builder.current_asset_doc + current_folder_entity = self.builder.current_folder_entity folder_path_regex = placeholder.data["folder_path"] product_name_regex_value = placeholder.data["product_name"] @@ -1492,12 +1473,17 @@ class PlaceholderLoadMixin(object): builder_type = placeholder.data["builder_type"] folder_ids = [] if builder_type == "context_folder": - folder_ids = [current_asset_doc["_id"]] + folder_ids = [current_folder_entity["id"]] elif builder_type == "all_folders": - folder_ids = list(self._query_by_folder_regex( - project_name, folder_path_regex - )) + folder_ids = { + folder_entity["id"] + for folder_entity in get_folders( + project_name, + folder_path_regex=folder_path_regex, + fields={"id"} + ) + } if not folder_ids: return [] @@ -1511,8 +1497,8 @@ class PlaceholderLoadMixin(object): filtered_product_ids = set() for product in products: if ( - product_name_regex is None - or product_name_regex.match(product["name"]) + product_name_regex is None + or product_name_regex.match(product["name"]) ): filtered_product_ids.add(product["id"]) @@ -1527,7 +1513,7 @@ class PlaceholderLoadMixin(object): ) return list(get_representations( project_name, - representation_names=[representation_name], + representation_names={representation_name}, version_ids=version_ids )) @@ -1543,26 +1529,29 @@ class PlaceholderLoadMixin(object): pass - def _reduce_last_version_repre_docs(self, representations): + def _reduce_last_version_repre_entities(self, representations): """Reduce representations to last verison.""" mapping = {} - for repre_doc in representations: - repre_context = repre_doc["context"] + # TODO use representation context with entities + # - using 'folder', 'subset' and 'version' from context on + # representation is danger + for repre_entity in representations: + repre_context = repre_entity["context"] - asset_name = repre_context["asset"] + folder_name = repre_context["asset"] product_name = repre_context["subset"] version = repre_context.get("version", -1) - if asset_name not in mapping: - mapping[asset_name] = {} + if folder_name not in mapping: + mapping[folder_name] = {} - product_mapping = mapping[asset_name] + product_mapping = mapping[folder_name] if product_name not in product_mapping: product_mapping[product_name] = collections.defaultdict(list) version_mapping = product_mapping[product_name] - version_mapping[version].append(repre_doc) + version_mapping[version].append(repre_entity) output = [] for product_mapping in mapping.values(): @@ -1599,10 +1588,10 @@ class PlaceholderLoadMixin(object): placeholder_representations = self._get_representations(placeholder) filtered_representations = [] - for representation in self._reduce_last_version_repre_docs( + for representation in self._reduce_last_version_repre_entities( placeholder_representations ): - repre_id = str(representation["_id"]) + repre_id = representation["id"] if repre_id not in ignore_repre_ids: filtered_representations.append(representation) @@ -1612,7 +1601,7 @@ class PlaceholderLoadMixin(object): ).format(placeholder.scene_identifier)) return - repre_load_contexts = get_contexts_for_repre_docs( + repre_load_contexts = get_representation_contexts( self.project_name, filtered_representations ) loaders_by_name = self.builder.get_loaders_by_name() @@ -1622,16 +1611,17 @@ class PlaceholderLoadMixin(object): failed = False for repre_load_context in repre_load_contexts.values(): + folder_path = repre_load_context["folder"]["path"] + product_name = repre_load_context["product"]["name"] representation = repre_load_context["representation"] - repre_context = representation["context"] self._before_repre_load( placeholder, representation ) self.log.info( "Loading {} from {} with loader {}\n" "Loader arguments used : {}".format( - repre_context["subset"], - repre_context["asset"], + product_name, + folder_path, loader_name, placeholder.data["loader_args"], ) @@ -1788,31 +1778,24 @@ class PlaceholderCreateMixin(object): # create product name context = self._builder.get_current_context() project_name = context["project_name"] - asset_name = context["folder_path"] + folder_path = context["folder_path"] task_name = context["task_name"] + host_name = self.builder.host_name - if legacy_create: - asset_doc = get_asset_by_name( - project_name, asset_name, fields=["_id"] - ) - assert asset_doc, "No current asset found in Session" - product_name = creator_plugin.get_product_name( - project_name, - asset_doc["_id"], - task_name, - create_variant, - ) + folder_entity = get_folder_by_path(project_name, folder_path) + if not folder_entity: + raise ValueError("Current context does not have set folder") + task_entity = get_task_by_name( + project_name, folder_entity["id"], task_name + ) - else: - asset_doc = get_asset_by_name(project_name, asset_name) - assert asset_doc, "No current asset found in Session" - product_name = creator_plugin.get_product_name( - project_name, - asset_doc, - task_name, - create_variant, - self.builder.host_name - ) + product_name = creator_plugin.get_product_name( + project_name, + folder_entity, + task_entity, + create_variant, + host_name + ) creator_data = { "creator_name": creator_name, @@ -1828,13 +1811,13 @@ class PlaceholderCreateMixin(object): if legacy_create: creator_instance = creator_plugin( product_name, - asset_name + folder_path ).process() else: creator_instance = self.builder.create_context.create( creator_plugin.identifier, create_variant, - asset_doc, + folder_entity, task_name=task_name, pre_create_data=pre_create_data ) diff --git a/client/ayon_core/plugins/actions/open_file_explorer.py b/client/ayon_core/plugins/actions/open_file_explorer.py index fba3c231a5..c221752f11 100644 --- a/client/ayon_core/plugins/actions/open_file_explorer.py +++ b/client/ayon_core/plugins/actions/open_file_explorer.py @@ -1,12 +1,10 @@ import os import platform import subprocess - from string import Formatter -from ayon_core.client import ( - get_project, - get_asset_by_name, -) + +import ayon_api + from ayon_core.pipeline import ( Anatomy, LauncherAction, @@ -28,10 +26,10 @@ class OpenTaskPath(LauncherAction): from qtpy import QtCore, QtWidgets project_name = session["AYON_PROJECT_NAME"] - asset_name = session["AYON_FOLDER_PATH"] + folder_path = session["AYON_FOLDER_PATH"] task_name = session.get("AYON_TASK_NAME", None) - path = self._get_workdir(project_name, asset_name, task_name) + path = self._get_workdir(project_name, folder_path, task_name) if not path: return @@ -62,11 +60,14 @@ class OpenTaskPath(LauncherAction): path = path.split(field, 1)[0] return path - def _get_workdir(self, project_name, asset_name, task_name): - project = get_project(project_name) - asset = get_asset_by_name(project_name, asset_name) + def _get_workdir(self, project_name, folder_path, task_name): + project_entity = ayon_api.get_project(project_name) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) - data = get_template_data(project, asset, task_name) + data = get_template_data(project_entity, folder_entity, task_entity) anatomy = Anatomy(project_name) workdir = anatomy.templates_obj["work"]["folder"].format(data) diff --git a/client/ayon_core/plugins/inventory/remove_and_load.py b/client/ayon_core/plugins/inventory/remove_and_load.py index 5529090b42..6553f9a7b3 100644 --- a/client/ayon_core/plugins/inventory/remove_and_load.py +++ b/client/ayon_core/plugins/inventory/remove_and_load.py @@ -1,12 +1,12 @@ -from ayon_core.pipeline import InventoryAction -from ayon_core.pipeline import get_current_project_name +import ayon_api + +from ayon_core.pipeline import get_current_project_name, InventoryAction from ayon_core.pipeline.load.plugins import discover_loader_plugins from ayon_core.pipeline.load.utils import ( get_loader_identifier, remove_container, load_container, ) -from ayon_core.client import get_representation_by_id class RemoveAndLoad(InventoryAction): @@ -21,6 +21,7 @@ class RemoveAndLoad(InventoryAction): get_loader_identifier(plugin): plugin for plugin in discover_loader_plugins(project_name=project_name) } + repre_ids = set() for container in containers: # Get loader loader_name = container["loader"] @@ -30,16 +31,23 @@ class RemoveAndLoad(InventoryAction): "Failed to get loader '{}', can't remove " "and load container".format(loader_name) ) + repre_ids.add(container["representation"]) - # Get representation - representation = get_representation_by_id( - project_name, container["representation"] + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in ayon_api.get_representations( + project_name, representation_ids=repre_ids ) - if not representation: + } + for container in containers: + # Get representation + repre_id = container["representation"] + repre_entity = repre_entities_by_id.get(repre_id) + if not repre_entity: self.log.warning( "Skipping remove and load because representation id is not" " found in database: '{}'".format( - container["representation"] + repre_id ) ) continue @@ -48,4 +56,4 @@ class RemoveAndLoad(InventoryAction): remove_container(container) # Load container - load_container(loader, representation) + load_container(loader, repre_entity) diff --git a/client/ayon_core/plugins/load/delete_old_versions.py b/client/ayon_core/plugins/load/delete_old_versions.py index 4fc61ebb8b..8fa0c2edb6 100644 --- a/client/ayon_core/plugins/load/delete_old_versions.py +++ b/client/ayon_core/plugins/load/delete_old_versions.py @@ -1,16 +1,16 @@ # TODO This plugin is not converted for AYON - +# # import collections # import os # import uuid # # import clique +# import ayon_api # from pymongo import UpdateOne # import qargparse # from qtpy import QtWidgets, QtCore # # from ayon_core import style -# from ayon_core.client import get_versions, get_representations # from ayon_core.addon import AddonsManager # from ayon_core.lib import format_file_size # from ayon_core.pipeline import load, Anatomy @@ -20,7 +20,7 @@ # ) # # -# class DeleteOldVersions(load.SubsetLoaderPlugin): +# class DeleteOldVersions(load.ProductLoaderPlugin): # """Deletes specific number of old version""" # # is_multiple_contexts_compatible = True @@ -196,19 +196,21 @@ # msgBox.exec_() # # def get_data(self, context, versions_count): -# subset = context["subset"] -# asset = context["asset"] +# product_entity = context["product"] +# folder_entity = context["folder"] # project_name = context["project"]["name"] # anatomy = Anatomy(project_name) # -# versions = list(get_versions(project_name, subset_ids=[subset["_id"]])) +# versions = list(ayon_api.get_versions( +# project_name, product_ids=[product_entity["id"]] +# )) # # versions_by_parent = collections.defaultdict(list) # for ent in versions: -# versions_by_parent[ent["parent"]].append(ent) +# versions_by_parent[ent["productId"]].append(ent) # # def sort_func(ent): -# return int(ent["name"]) +# return int(ent["version"]) # # all_last_versions = [] # for _parent_id, _versions in versions_by_parent.items(): @@ -228,7 +230,7 @@ # # Update versions_by_parent without filtered versions # versions_by_parent = collections.defaultdict(list) # for ent in versions: -# versions_by_parent[ent["parent"]].append(ent) +# versions_by_parent[ent["productId"]].append(ent) # # # Filter already deleted versions # versions_to_pop = [] @@ -238,15 +240,17 @@ # versions_to_pop.append(version) # # for version in versions_to_pop: -# msg = "Asset: \"{}\" | Subset: \"{}\" | Version: \"{}\"".format( -# asset["name"], subset["name"], version["name"] +# msg = "Folder: \"{}\" | Product: \"{}\" | Version: \"{}\"".format( +# folder_entity["path"], +# product_entity["name"], +# version["version"] # ) # self.log.debug(( # "Skipping version. Already tagged as `deleted`. < {} >" # ).format(msg)) # versions.remove(version) # -# version_ids = [ent["_id"] for ent in versions] +# version_ids = [ent["id"] for ent in versions] # # self.log.debug( # "Filtered versions to delete ({})".format(len(version_ids)) @@ -254,13 +258,13 @@ # # if not version_ids: # msg = "Skipping processing. Nothing to delete on {}/{}".format( -# asset["name"], subset["name"] +# folder_entity["path"], product_entity["name"] # ) # self.log.info(msg) # print(msg) # return # -# repres = list(get_representations( +# repres = list(ayon_api.get_representations( # project_name, version_ids=version_ids # )) # @@ -271,7 +275,9 @@ # dir_paths = {} # file_paths_by_dir = collections.defaultdict(list) # for repre in repres: -# file_path, seq_path = self.path_from_representation(repre, anatomy) +# file_path, seq_path = self.path_from_representation( +# repre, anatomy +# ) # if file_path is None: # self.log.debug(( # "Could not format path for represenation \"{}\"" @@ -310,17 +316,15 @@ # "Folder does not exist. Deleting it's files skipped: {}" # ).format(paths_msg)) # -# data = { +# return { # "dir_paths": dir_paths, # "file_paths_by_dir": file_paths_by_dir, # "versions": versions, -# "asset": asset, -# "subset": subset, -# "archive_subset": versions_count == 0 +# "folder": folder_entity, +# "product": product_entity, +# "archive_product": versions_count == 0 # } # -# return data -# # def main(self, project_name, data, remove_publish_folder): # # Size of files. # size = 0 @@ -344,14 +348,14 @@ # if version_tags == orig_version_tags: # continue # -# update_query = {"_id": version["_id"]} +# update_query = {"id": version["id"]} # update_data = {"$set": {"data.tags": version_tags}} # mongo_changes_bulk.append(UpdateOne(update_query, update_data)) # -# if data["archive_subset"]: +# if data["archive_product"]: # mongo_changes_bulk.append(UpdateOne( # { -# "_id": data["subset"]["_id"], +# "id": data["product"]["id"], # "type": "subset" # }, # {"$set": {"type": "archived_subset"}} @@ -379,15 +383,15 @@ # "not published" which cause that they're invisible. # # Args: -# data (dict): Data sent to subset loader with full context. +# data (dict): Data sent to product loader with full context. # """ # -# # First check for ftrack id on asset document +# # First check for ftrack id on folder entity # # - skip if ther is none -# asset_ftrack_id = data["asset"]["data"].get("ftrackId") -# if not asset_ftrack_id: +# ftrack_id = data["folder"]["attrib"].get("ftrackId") +# if not ftrack_id: # self.log.info(( -# "Asset does not have filled ftrack id. Skipped delete" +# "Folder does not have filled ftrack id. Skipped delete" # " of ftrack version." # )) # return @@ -401,7 +405,7 @@ # import ftrack_api # # session = ftrack_api.Session() -# product_name = data["subset"]["name"] +# product_name = data["product"]["name"] # versions = { # '"{}"'.format(version_doc["name"]) # for version_doc in data["versions"] @@ -413,7 +417,7 @@ # " and asset.name is \"{}\"" # " and version in ({})" # ).format( -# asset_ftrack_id, +# ftrack_id, # product_name, # ",".join(versions) # ) diff --git a/client/ayon_core/plugins/load/delivery.py b/client/ayon_core/plugins/load/delivery.py index 16f315937b..453bdfb87a 100644 --- a/client/ayon_core/plugins/load/delivery.py +++ b/client/ayon_core/plugins/load/delivery.py @@ -2,9 +2,9 @@ import copy import platform from collections import defaultdict +import ayon_api from qtpy import QtWidgets, QtCore, QtGui -from ayon_core.client import get_representations from ayon_core.pipeline import load, Anatomy from ayon_core import resources, style @@ -22,7 +22,7 @@ from ayon_core.pipeline.delivery import ( ) -class Delivery(load.SubsetLoaderPlugin): +class Delivery(load.ProductLoaderPlugin): """Export selected versions to folder structure from Template""" is_multiple_contexts_compatible = True @@ -202,7 +202,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): ) anatomy_data = copy.deepcopy(repre["context"]) - new_report_items = check_destination_path(str(repre["_id"]), + new_report_items = check_destination_path(repre["id"], self.anatomy, anatomy_data, datetime_data, @@ -260,7 +260,7 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): report_items.update(new_report_items) self._update_progress(uploaded) else: # fallback for Pype2 and representations without files - frame = repre['context'].get('frame') + frame = repre["context"].get("frame") if frame: repre["context"]["frame"] = len(str(frame)) * "#" @@ -290,9 +290,9 @@ class DeliveryOptionsDialog(QtWidgets.QDialog): return templates def _set_representations(self, project_name, contexts): - version_ids = [context["version"]["_id"] for context in contexts] + version_ids = {context["version"]["id"] for context in contexts} - repres = list(get_representations( + repres = list(ayon_api.get_representations( project_name, version_ids=version_ids )) diff --git a/client/ayon_core/plugins/load/push_to_library.py b/client/ayon_core/plugins/load/push_to_library.py index 39f95d134c..a191ee88f0 100644 --- a/client/ayon_core/plugins/load/push_to_library.py +++ b/client/ayon_core/plugins/load/push_to_library.py @@ -6,7 +6,7 @@ from ayon_core.pipeline import load from ayon_core.pipeline.load import LoadError -class PushToLibraryProject(load.SubsetLoaderPlugin): +class PushToLibraryProject(load.ProductLoaderPlugin): """Export selected versions to folder structure from Template""" is_multiple_contexts_compatible = True @@ -40,10 +40,8 @@ class PushToLibraryProject(load.SubsetLoaderPlugin): "main.py" ) - project_doc = context["project"] - version_doc = context["version"] - project_name = project_doc["name"] - version_id = str(version_doc["_id"]) + project_name = context["project"]["name"] + version_id = context["version"]["id"] args = get_ayon_launcher_args( "run", 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 b5bb579498..cccf392e40 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_context_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_context_data.py @@ -3,7 +3,8 @@ Requires: context -> anatomy context -> projectEntity - context -> assetEntity + context -> folderEntity + context -> taskEntity context -> task context -> username context -> datetimeData @@ -49,15 +50,15 @@ class CollectAnatomyContextData(pyblish.api.ContextPlugin): host_name = context.data["hostName"] project_settings = context.data["project_settings"] project_entity = context.data["projectEntity"] - asset_entity = context.data.get("assetEntity") - task_name = None - if asset_entity: - task_name = context.data["task"] + folder_entity = context.data.get("folderEntity") + task_entity = None + if folder_entity: + task_entity = context.data["taskEntity"] anatomy_data = get_template_data( project_entity, - asset_entity, - task_name, + folder_entity, + task_entity, host_name, project_settings ) diff --git a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py index b62935dd6a..e3b27a0db5 100644 --- a/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py +++ b/client/ayon_core/plugins/publish/collect_anatomy_instance_data.py @@ -1,13 +1,17 @@ """ Requires: - context -> anatomyData + context -> projectName context -> projectEntity - context -> assetEntity + context -> anatomyData instance -> folderPath instance -> productName instance -> productType Optional: + context -> folderEntity + context -> taskEntity + instance -> task + instance -> taskEntity instance -> version instance -> resolutionWidth instance -> resolutionHeight @@ -15,7 +19,8 @@ Optional: Provides: instance -> projectEntity - instance -> assetEntity + instance -> folderEntity + instance -> taskEntity instance -> anatomyData instance -> version instance -> latestVersion @@ -26,13 +31,8 @@ import json import collections import pyblish.api +import ayon_api -from ayon_core.client import ( - get_assets, - get_subsets, - get_last_versions, - get_asset_name_identifier, -) from ayon_core.pipeline.version_start import get_versioning_start @@ -51,73 +51,174 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): self.log.debug("Collecting anatomy data for all instances.") project_name = context.data["projectName"] - self.fill_missing_asset_docs(context, project_name) + self.fill_missing_folder_entities(context, project_name) + self.fill_missing_task_entities(context, project_name) self.fill_latest_versions(context, project_name) self.fill_anatomy_data(context) self.log.debug("Anatomy Data collection finished.") - def fill_missing_asset_docs(self, context, project_name): - self.log.debug("Querying asset documents for instances.") + def fill_missing_folder_entities(self, context, project_name): + self.log.debug("Querying folder entities for instances.") - context_asset_doc = context.data.get("assetEntity") - context_asset_name = None - if context_asset_doc: - context_asset_name = get_asset_name_identifier(context_asset_doc) + context_folder_entity = context.data.get("folderEntity") + context_folder_path = None + if context_folder_entity: + context_folder_path = context_folder_entity["path"] - instances_with_missing_asset_doc = collections.defaultdict(list) + instances_missing_folder = collections.defaultdict(list) for instance in context: - instance_asset_doc = instance.data.get("assetEntity") - _asset_name = instance.data["folderPath"] + instance_folder_entity = instance.data.get("folderEntity") + _folder_path = instance.data["folderPath"] - # There is possibility that assetEntity on instance is already set - # which can happen in standalone publisher - if instance_asset_doc: - instance_asset_name = get_asset_name_identifier( - instance_asset_doc) - if instance_asset_name == _asset_name: + # There is possibility that folderEntity on instance is set + if instance_folder_entity: + instance_folder_path = instance_folder_entity["path"] + if instance_folder_path == _folder_path: continue - # Check if asset name is the same as what is in context - # - they may be different, e.g. in NukeStudio - if context_asset_name and context_asset_name == _asset_name: - instance.data["assetEntity"] = context_asset_doc + # Check if folder path is the same as what is in context + # - they may be different, e.g. during editorial publishing + if context_folder_path and context_folder_path == _folder_path: + instance.data["folderEntity"] = context_folder_entity else: - instances_with_missing_asset_doc[_asset_name].append(instance) + instances_missing_folder[_folder_path].append( + instance + ) - if not instances_with_missing_asset_doc: - self.log.debug("All instances already had right asset document.") + if not instances_missing_folder: + self.log.debug("All instances already had right folder entity.") return - asset_names = list(instances_with_missing_asset_doc.keys()) - self.log.debug("Querying asset documents with names: {}".format( - ", ".join(["\"{}\"".format(name) for name in asset_names]) + folder_paths = list(instances_missing_folder.keys()) + self.log.debug("Querying folder entities with paths: {}".format( + ", ".join(["\"{}\"".format(path) for path in folder_paths]) )) - asset_docs = get_assets(project_name, asset_names=asset_names) - asset_docs_by_name = { - get_asset_name_identifier(asset_doc): asset_doc - for asset_doc in asset_docs + folder_entities_by_path = { + folder_entity["path"]: folder_entity + for folder_entity in ayon_api.get_folders( + project_name, folder_paths=folder_paths + ) } - not_found_asset_names = [] - for asset_name, instances in instances_with_missing_asset_doc.items(): - asset_doc = asset_docs_by_name.get(asset_name) - if not asset_doc: - not_found_asset_names.append(asset_name) + not_found_folder_paths = [] + for folder_path, instances in instances_missing_folder.items(): + folder_entity = folder_entities_by_path.get(folder_path) + if not folder_entity: + not_found_folder_paths.append(folder_path) continue for _instance in instances: - _instance.data["assetEntity"] = asset_doc + _instance.data["folderEntity"] = folder_entity - if not_found_asset_names: - joined_asset_names = ", ".join( - ["\"{}\"".format(name) for name in not_found_asset_names] + if not_found_folder_paths: + joined_folder_paths = ", ".join( + ["\"{}\"".format(path) for path in not_found_folder_paths] ) self.log.warning(( - "Not found asset documents with names \"{}\"." - ).format(joined_asset_names)) + "Not found folder entities with paths \"{}\"." + ).format(joined_folder_paths)) + + def fill_missing_task_entities(self, context, project_name): + self.log.debug("Querying task entities for instances.") + + context_folder_entity = context.data.get("folderEntity") + context_folder_id = None + if context_folder_entity: + context_folder_id = context_folder_entity["id"] + context_task_entity = context.data.get("taskEntity") + context_task_name = None + if context_task_entity: + context_task_name = context_task_entity["name"] + + instances_missing_task = {} + folder_path_by_id = {} + for instance in context: + folder_entity = instance.data.get("folderEntity") + # Skip if instnace does not have filled folder entity + if not folder_entity: + continue + folder_id = folder_entity["id"] + folder_path_by_id[folder_id] = folder_entity["path"] + + task_entity = instance.data.get("taskEntity") + _task_name = instance.data.get("task") + + # There is possibility that taskEntity on instance is set + if task_entity: + task_parent_id = task_entity["folderId"] + instance_task_name = task_entity["name"] + if ( + folder_id == task_parent_id + and instance_task_name == _task_name + ): + continue + + # Check if folder path is the same as what is in context + # - they may be different, e.g. in NukeStudio + if ( + context_folder_id == folder_id + and context_task_name == _task_name + ): + instance.data["taskEntity"] = context_task_entity + continue + + _by_folder_id = instances_missing_task.setdefault(folder_id, {}) + _by_task_name = _by_folder_id.setdefault(_task_name, []) + _by_task_name.append(instance) + + if not instances_missing_task: + self.log.debug("All instances already had right task entity.") + return + + self.log.debug("Querying task entities") + + all_folder_ids = set(instances_missing_task.keys()) + all_task_names = set() + for per_task in instances_missing_task.values(): + all_task_names |= set(per_task.keys()) + all_task_names.discard(None) + + task_entities = [] + if all_task_names: + task_entities = ayon_api.get_tasks( + project_name, + folder_ids=all_folder_ids, + task_names=all_task_names + ) + task_entity_by_ids = {} + for task_entity in task_entities: + folder_id = task_entity["folderId"] + task_name = task_entity["name"] + _by_folder_id = task_entity_by_ids.setdefault(folder_id, {}) + _by_folder_id[task_name] = task_entity + + not_found_task_paths = [] + for folder_id, by_task in instances_missing_task.items(): + for task_name, instances in by_task.items(): + task_entity = ( + task_entity_by_ids + .get(folder_id, {}) + .get(task_name) + ) + if task_name and not task_entity: + folder_path = folder_path_by_id[folder_id] + not_found_task_paths.append( + "/".join([folder_path, task_name]) + ) + + for instance in instances: + instance.data["taskEntity"] = task_entity + + if not_found_task_paths: + joined_paths = ", ".join( + ["\"{}\"".format(path) for path in not_found_task_paths] + ) + self.log.warning(( + "Not found task entities with paths \"{}\"." + ).format(joined_paths)) def fill_latest_versions(self, context, project_name): """Try to find latest version for each instance's product name. @@ -140,13 +241,13 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): latest_version = instance.data.get("latestVersion") instance.data["latestVersion"] = latest_version - # Skip instances without "assetEntity" - asset_doc = instance.data.get("assetEntity") - if not asset_doc: + # Skip instances without "folderEntity" + folder_entity = instance.data.get("folderEntity") + if not folder_entity: continue # Store folder ids and product names for queries - folder_id = asset_doc["_id"] + folder_id = folder_entity["id"] product_name = instance.data["productName"] # Prepare instance hierarchy for faster filling latest versions @@ -157,37 +258,41 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): hierarchy[folder_id][product_name].append(instance) names_by_folder_ids[folder_id].add(product_name) - subset_docs = [] + product_entities = [] if names_by_folder_ids: - subset_docs = list(get_subsets( - project_name, names_by_asset_ids=names_by_folder_ids + product_entities = list(ayon_api.get_products( + project_name, names_by_folder_ids=names_by_folder_ids )) product_ids = { - subset_doc["_id"] - for subset_doc in subset_docs + product_entity["id"] + for product_entity in product_entities } - last_version_docs_by_product_id = get_last_versions( - project_name, product_ids, fields=["name"] + last_versions_by_product_id = ayon_api.get_last_versions( + project_name, product_ids, fields={"version"} ) - for subset_doc in subset_docs: - product_id = subset_doc["_id"] - last_version_doc = last_version_docs_by_product_id.get(product_id) - if last_version_doc is None: + for product_entity in product_entities: + product_id = product_entity["id"] + last_version_entity = last_versions_by_product_id.get(product_id) + if last_version_entity is None: continue - folder_id = subset_doc["parent"] - product_name = subset_doc["name"] + last_version = last_version_entity["version"] + folder_id = product_entity["folderId"] + product_name = product_entity["name"] _instances = hierarchy[folder_id][product_name] for _instance in _instances: - _instance.data["latestVersion"] = last_version_doc["name"] + _instance.data["latestVersion"] = last_version def fill_anatomy_data(self, context): self.log.debug("Storing anatomy data to instance data.") - project_doc = context.data["projectEntity"] - project_task_types = project_doc["config"]["tasks"] + project_entity = context.data["projectEntity"] + task_types_by_name = { + task_type["name"]: task_type + for task_type in project_entity["taskTypes"] + } for instance in context: anatomy_data = copy.deepcopy(context.data["anatomyData"]) @@ -202,8 +307,8 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): } }) - self._fill_asset_data(instance, project_doc, anatomy_data) - self._fill_task_data(instance, project_task_types, anatomy_data) + self._fill_folder_data(instance, project_entity, anatomy_data) + self._fill_task_data(instance, task_types_by_name, anatomy_data) # Define version version_number = None @@ -258,7 +363,7 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): anatomy_data["fps"] = float("{:0.2f}".format(float(fps))) # Store anatomy data - instance.data["projectEntity"] = project_doc + instance.data["projectEntity"] = project_entity instance.data["anatomyData"] = anatomy_data instance.data["version"] = version_number @@ -272,24 +377,28 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): json.dumps(anatomy_data, indent=4) )) - def _fill_asset_data(self, instance, project_doc, anatomy_data): - # QUESTION should we make sure that all asset data are poped if asset - # data cannot be found? - # - 'asset', 'hierarchy', 'parent', 'folder' - asset_doc = instance.data.get("assetEntity") - if asset_doc: - parents = asset_doc["data"].get("parents") or list() - parent_name = project_doc["name"] - if parents: - parent_name = parents[-1] + def _fill_folder_data(self, instance, project_entity, anatomy_data): + # QUESTION should we make sure that all folder data are poped if + # folder data cannot be found? + # - 'folder', 'hierarchy', 'parent', 'folder' + folder_entity = instance.data.get("folderEntity") + if folder_entity: + folder_name = folder_entity["name"] + folder_path = folder_entity["path"] + hierarchy_parts = folder_path.split("/") + hierarchy_parts.pop(0) + hierarchy_parts.pop(-1) + parent_name = project_entity["name"] + if hierarchy_parts: + parent_name = hierarchy_parts[-1] - hierarchy = "/".join(parents) + hierarchy = "/".join(hierarchy_parts) anatomy_data.update({ - "asset": asset_doc["name"], + "asset": folder_name, "hierarchy": hierarchy, "parent": parent_name, "folder": { - "name": asset_doc["name"], + "name": folder_name, }, }) return @@ -298,21 +407,21 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): hierarchy = instance.data["hierarchy"] anatomy_data["hierarchy"] = hierarchy - parent_name = project_doc["name"] + parent_name = project_entity["name"] if hierarchy: parent_name = hierarchy.split("/")[-1] - asset_name = instance.data["folderPath"].split("/")[-1] + folder_name = instance.data["folderPath"].split("/")[-1] anatomy_data.update({ - "asset": asset_name, + "asset": folder_name, "hierarchy": hierarchy, "parent": parent_name, "folder": { - "name": asset_name, + "name": folder_name, }, }) - def _fill_task_data(self, instance, project_task_types, anatomy_data): + def _fill_task_data(self, instance, task_types_by_name, anatomy_data): # QUESTION should we make sure that all task data are poped if task # data cannot be resolved? # - 'task' @@ -322,10 +431,10 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): if not task_name: return - # Find task data based on asset entity - asset_doc = instance.data.get("assetEntity") - task_data = self._get_task_data_from_asset( - asset_doc, task_name, project_task_types + # Find task data based on folder entity + task_entity = instance.data.get("taskEntity") + task_data = self._get_task_data_from_entity( + task_entity, task_types_by_name ) if task_data: # Fill task data @@ -341,20 +450,20 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): if not instance.data.get("newAssetPublishing"): return - # Try to find task data based on hierarchy context and asset name + # Try to find task data based on hierarchy context and folder path hierarchy_context = instance.context.data.get("hierarchyContext") - asset_name = instance.data.get("folderPath") - if not hierarchy_context or not asset_name: + folder_path = instance.data.get("folderPath") + if not hierarchy_context or not folder_path: return project_name = instance.context.data["projectName"] - if "/" not in asset_name: + if "/" not in folder_path: tasks_info = self._find_tasks_info_in_hierarchy( - hierarchy_context, asset_name + hierarchy_context, folder_path ) else: current_data = hierarchy_context.get(project_name, {}) - for key in asset_name.split("/"): + for key in folder_path.split("/"): if key: current_data = current_data.get("childs", {}).get(key, {}) tasks_info = current_data.get("tasks", {}) @@ -362,9 +471,9 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): task_info = tasks_info.get(task_name, {}) task_type = task_info.get("type") task_code = ( - project_task_types + task_types_by_name .get(task_type, {}) - .get("short_name") + .get("shortName") ) anatomy_data["task"] = { "name": task_name, @@ -372,43 +481,41 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): "short": task_code } - def _get_task_data_from_asset( - self, asset_doc, task_name, project_task_types + def _get_task_data_from_entity( + self, task_entity, task_types_by_name ): """ Args: - asset_doc (Union[dict[str, Any], None]): Asset document. - task_name (Union[str, None]): Task name. - project_task_types (dict[str, dict[str, Any]]): Project task + task_entity (Union[dict[str, Any], None]): Task entity. + task_types_by_name (dict[str, dict[str, Any]]): Project task types. Returns: Union[dict[str, str], None]: Task data or None if not found. """ - if not asset_doc or not task_name: + if not task_entity: return None - asset_tasks = asset_doc["data"]["tasks"] - task_type = asset_tasks.get(task_name, {}).get("type") + task_type = task_entity["taskType"] task_code = ( - project_task_types + task_types_by_name .get(task_type, {}) - .get("short_name") + .get("shortName") ) return { - "name": task_name, + "name": task_entity["name"], "type": task_type, "short": task_code } - def _find_tasks_info_in_hierarchy(self, hierarchy_context, asset_name): + def _find_tasks_info_in_hierarchy(self, hierarchy_context, folder_name): """Find tasks info for an asset in editorial hierarchy. Args: hierarchy_context (dict[str, Any]): Editorial hierarchy context. - asset_name (str): Asset name. + folder_name (str): Folder name. Returns: dict[str, dict[str, Any]]: Tasks info by name. @@ -418,8 +525,8 @@ class CollectAnatomyInstanceData(pyblish.api.ContextPlugin): hierarchy_queue.append(copy.deepcopy(hierarchy_context)) while hierarchy_queue: item = hierarchy_queue.popleft() - if asset_name in item: - return item[asset_name].get("tasks") or {} + if folder_name in item: + return item[folder_name].get("tasks") or {} for subitem in item.values(): hierarchy_queue.extend(subitem.get("childs") or []) diff --git a/client/ayon_core/plugins/publish/collect_audio.py b/client/ayon_core/plugins/publish/collect_audio.py index 58419e7284..c1633e414e 100644 --- a/client/ayon_core/plugins/publish/collect_audio.py +++ b/client/ayon_core/plugins/publish/collect_audio.py @@ -1,18 +1,13 @@ import collections + +import ayon_api import pyblish.api -from ayon_core.client import ( - get_assets, - get_subsets, - get_last_versions, - get_representations, - get_asset_name_identifier, -) from ayon_core.pipeline.load import get_representation_path_with_anatomy class CollectAudio(pyblish.api.ContextPlugin): - """Collect asset's last published audio. + """Collect folders's last published audio. The audio product name searched for is defined in: project settings > Collect Audio @@ -23,7 +18,7 @@ class CollectAudio(pyblish.api.ContextPlugin): converted to context plugin which requires only 4 queries top. """ - label = "Collect Asset Audio" + label = "Collect Folder Audio" order = pyblish.api.CollectorOrder + 0.1 families = ["review"] hosts = [ @@ -67,36 +62,36 @@ class CollectAudio(pyblish.api.ContextPlugin): return # Add audio to instance if exists. - instances_by_asset_name = collections.defaultdict(list) + instances_by_folder_path = collections.defaultdict(list) for instance in filtered_instances: - asset_name = instance.data["folderPath"] - instances_by_asset_name[asset_name].append(instance) + folder_path = instance.data["folderPath"] + instances_by_folder_path[folder_path].append(instance) - asset_names = set(instances_by_asset_name.keys()) + folder_paths = set(instances_by_folder_path.keys()) self.log.debug(( - "Searching for audio product '{product}' in assets {assets}" + "Searching for audio product '{product}' in folders {folders}" ).format( product=self.audio_product_name, - assets=", ".join([ - '"{}"'.format(asset_name) - for asset_name in asset_names + folders=", ".join([ + '"{}"'.format(folder_path) + for folder_path in folder_paths ]) )) # Query all required documents project_name = context.data["projectName"] anatomy = context.data["anatomy"] - repre_docs_by_asset_names = self.query_representations( - project_name, asset_names) + repre_entities_by_folder_paths = self.query_representations( + project_name, folder_paths) - for asset_name, instances in instances_by_asset_name.items(): - repre_docs = repre_docs_by_asset_names[asset_name] - if not repre_docs: + for folder_path, instances in instances_by_folder_path.items(): + repre_entities = repre_entities_by_folder_paths[folder_path] + if not repre_entities: continue - repre_doc = repre_docs[0] + repre_entity = repre_entities[0] repre_path = get_representation_path_with_anatomy( - repre_doc, anatomy + repre_entity, anatomy ) for instance in instances: instance.data["audio"] = [{ @@ -106,7 +101,7 @@ class CollectAudio(pyblish.api.ContextPlugin): self.log.debug("Audio Data added to instance ...") def query_representations(self, project_name, folder_paths): - """Query representations related to audio products for passed assets. + """Query representations related to audio products for passed folders. Args: project_name (str): Project in which we're looking for all @@ -116,67 +111,74 @@ class CollectAudio(pyblish.api.ContextPlugin): Returns: collections.defaultdict[str, List[Dict[Str, Any]]]: Representations - related to audio products by asset name. - """ + related to audio products by folder path. + """ output = collections.defaultdict(list) - # Query asset documents - asset_docs = get_assets( + # Skip the queries if audio product name is not defined + if not self.audio_product_name: + return output + + # Query folder entities + folder_entities = ayon_api.get_folders( project_name, - asset_names=folder_paths, - fields=["_id", "name", "data.parents"] + folder_paths=folder_paths, + fields={"id", "path"} ) folder_id_by_path = { - get_asset_name_identifier(asset_doc): asset_doc["_id"] - for asset_doc in asset_docs + folder_entity["path"]: folder_entity["id"] + for folder_entity in folder_entities } folder_ids = set(folder_id_by_path.values()) # Query products with name define by 'audio_product_name' attr - # - one or none products with the name should be available on an asset - subset_docs = get_subsets( + # - one or none products with the name should be available on + # an folder + product_entities = ayon_api.get_products( project_name, - subset_names=[self.audio_product_name], - asset_ids=folder_ids, - fields=["_id", "parent"] + product_names=[self.audio_product_name], + folder_ids=folder_ids, + fields={"id", "folderId"} ) product_id_by_folder_id = {} - for subset_doc in subset_docs: - folder_id = subset_doc["parent"] - product_id_by_folder_id[folder_id] = subset_doc["_id"] + for product_entity in product_entities: + folder_id = product_entity["folderId"] + product_id_by_folder_id[folder_id] = product_entity["id"] product_ids = set(product_id_by_folder_id.values()) if not product_ids: return output # Find all latest versions for the products - version_docs_by_product_id = get_last_versions( - project_name, subset_ids=product_ids, fields=["_id", "parent"] + last_versions_by_product_id = ayon_api.get_last_versions( + project_name, product_ids=product_ids, fields={"id", "productId"} ) version_id_by_product_id = { - product_id: version_doc["_id"] - for product_id, version_doc in version_docs_by_product_id.items() + product_id: version_entity["id"] + for product_id, version_entity in ( + last_versions_by_product_id.items() + ) } version_ids = set(version_id_by_product_id.values()) if not version_ids: return output # Find representations under latest versions of audio products - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, version_ids=version_ids ) - repre_docs_by_version_id = collections.defaultdict(list) - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - repre_docs_by_version_id[version_id].append(repre_doc) + repre_entities_by_version_id = collections.defaultdict(list) + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + repre_entities_by_version_id[version_id].append(repre_entity) - if not repre_docs_by_version_id: + if not repre_entities_by_version_id: return output for folder_path in folder_paths: folder_id = folder_id_by_path.get(folder_path) product_id = product_id_by_folder_id.get(folder_id) version_id = version_id_by_product_id.get(product_id) - output[folder_path] = repre_docs_by_version_id[version_id] + output[folder_path] = repre_entities_by_version_id[version_id] return output diff --git a/client/ayon_core/plugins/publish/collect_context_entities.py b/client/ayon_core/plugins/publish/collect_context_entities.py index 64ef73e2d9..f340178e4f 100644 --- a/client/ayon_core/plugins/publish/collect_context_entities.py +++ b/client/ayon_core/plugins/publish/collect_context_entities.py @@ -2,18 +2,20 @@ Requires: context -> projectName - context -> asset + context -> folderPath context -> task Provides: - context -> projectEntity - Project document from database. - context -> assetEntity - Asset document from database only if 'asset' is - set in context. + context -> projectEntity - Project entity from AYON server. + context -> folderEntity - Folder entity from AYON server only if + 'folderPath' is set in context data. + context -> taskEntity - Task entity from AYON server only if 'folderPath' + and 'task' are set in context data. """ import pyblish.api +import ayon_api -from ayon_core.client import get_project, get_asset_by_name from ayon_core.pipeline import KnownPublishError @@ -25,45 +27,48 @@ class CollectContextEntities(pyblish.api.ContextPlugin): def process(self, context): project_name = context.data["projectName"] - asset_name = context.data["folderPath"] + folder_path = context.data["folderPath"] task_name = context.data["task"] - project_entity = get_project(project_name) + project_entity = ayon_api.get_project(project_name) if not project_entity: raise KnownPublishError( - "Project '{0}' was not found.".format(project_name) + "Project '{}' was not found.".format(project_name) ) self.log.debug("Collected Project \"{}\"".format(project_entity)) context.data["projectEntity"] = project_entity - if not asset_name: + if not folder_path: self.log.info("Context is not set. Can't collect global data.") return - asset_entity = get_asset_by_name(project_name, asset_name) - assert asset_entity, ( - "No asset found by the name '{0}' in project '{1}'" - ).format(asset_name, project_name) + folder_entity = self._get_folder_entity(project_name, folder_path) + self.log.debug("Collected Folder \"{}\"".format(folder_entity)) - self.log.debug("Collected Asset \"{}\"".format(asset_entity)) + task_entity = self._get_task_entity( + project_name, folder_entity, task_name + ) + self.log.debug("Collected Task \"{}\"".format(task_entity)) - context.data["assetEntity"] = asset_entity + context.data["folderEntity"] = folder_entity + context.data["taskEntity"] = task_entity - data = asset_entity['data'] + folder_attributes = folder_entity["attrib"] # Task type - asset_tasks = data.get("tasks") or {} - task_info = asset_tasks.get(task_name) or {} - task_type = task_info.get("type") + task_type = None + if task_entity: + task_type = task_entity["taskType"] + context.data["taskType"] = task_type - frame_start = data.get("frameStart") + frame_start = folder_attributes.get("frameStart") if frame_start is None: frame_start = 1 self.log.warning("Missing frame start. Defaulting to 1.") - frame_end = data.get("frameEnd") + frame_end = folder_attributes.get("frameEnd") if frame_end is None: frame_end = 2 self.log.warning("Missing frame end. Defaulting to 2.") @@ -71,8 +76,8 @@ class CollectContextEntities(pyblish.api.ContextPlugin): context.data["frameStart"] = frame_start context.data["frameEnd"] = frame_end - handle_start = data.get("handleStart") or 0 - handle_end = data.get("handleEnd") or 0 + handle_start = folder_attributes.get("handleStart") or 0 + handle_end = folder_attributes.get("handleEnd") or 0 context.data["handleStart"] = int(handle_start) context.data["handleEnd"] = int(handle_end) @@ -82,4 +87,30 @@ class CollectContextEntities(pyblish.api.ContextPlugin): context.data["frameStartHandle"] = frame_start_h context.data["frameEndHandle"] = frame_end_h - context.data["fps"] = data["fps"] + context.data["fps"] = folder_attributes["fps"] + + def _get_folder_entity(self, project_name, folder_path): + if not folder_path: + return None + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + if not folder_entity: + raise KnownPublishError( + "Folder '{}' was not found in project '{}'.".format( + folder_path, project_name + ) + ) + return folder_entity + + def _get_task_entity(self, project_name, folder_entity, task_name): + if not folder_entity or not task_name: + return None + task_entity = ayon_api.get_task_by_name( + project_name, folder_entity["id"], task_name + ) + if not task_entity: + task_path = "/".join([folder_entity["path"], task_name]) + raise KnownPublishError( + "Task '{}' was not found in project '{}'.".format( + task_path, project_name) + ) + return task_entity \ No newline at end of file diff --git a/client/ayon_core/plugins/publish/collect_current_context.py b/client/ayon_core/plugins/publish/collect_current_context.py index 76d30a913e..76181ffc39 100644 --- a/client/ayon_core/plugins/publish/collect_current_context.py +++ b/client/ayon_core/plugins/publish/collect_current_context.py @@ -21,14 +21,14 @@ class CollectCurrentContext(pyblish.api.ContextPlugin): def process(self, context): # Check if values are already set project_name = context.data.get("projectName") - asset_name = context.data.get("folderPath") + folder_path = context.data.get("folderPath") task_name = context.data.get("task") current_context = get_current_context() if not project_name: context.data["projectName"] = current_context["project_name"] - if not asset_name: + if not folder_path: context.data["folderPath"] = current_context["folder_path"] if not task_name: @@ -40,10 +40,10 @@ class CollectCurrentContext(pyblish.api.ContextPlugin): self.log.info(( "Collected project context\n" "Project: {project_name}\n" - "Asset: {asset_name}\n" + "Folder: {folder_path}\n" "Task: {task_name}" ).format( project_name=context.data["projectName"], - asset_name=context.data["folderPath"], + folder_path=context.data["folderPath"], task_name=context.data["task"] )) diff --git a/client/ayon_core/plugins/publish/collect_custom_staging_dir.py b/client/ayon_core/plugins/publish/collect_custom_staging_dir.py index e42f34b0ae..49c3a98dd2 100644 --- a/client/ayon_core/plugins/publish/collect_custom_staging_dir.py +++ b/client/ayon_core/plugins/publish/collect_custom_staging_dir.py @@ -28,7 +28,7 @@ class CollectCustomStagingDir(pyblish.api.InstancePlugin): Location of the folder is configured in `project_anatomy/templates/others`. ('transient' key is expected, with 'folder' key) - Which family/task type/subset is applicable is configured in: + Which family/task type/product is applicable is configured in: `project_settings/global/tools/publish/custom_staging_dir_profiles` """ diff --git a/client/ayon_core/plugins/publish/collect_frames_fix.py b/client/ayon_core/plugins/publish/collect_frames_fix.py index 0fe86b8d70..0f7d5b692a 100644 --- a/client/ayon_core/plugins/publish/collect_frames_fix.py +++ b/client/ayon_core/plugins/publish/collect_frames_fix.py @@ -1,14 +1,11 @@ import pyblish.api +import ayon_api + from ayon_core.lib.attribute_definitions import ( TextDef, BoolDef ) - from ayon_core.pipeline.publish import AYONPyblishPluginMixin -from ayon_core.client.entities import ( - get_last_version_by_subset_name, - get_representations -) class CollectFramesFixDef( @@ -41,28 +38,34 @@ class CollectFramesFixDef( instance.data["frames_to_fix"] = frames_to_fix product_name = instance.data["productName"] - asset_name = instance.data["folderPath"] + folder_entity = instance.data["folderEntity"] project_entity = instance.data["projectEntity"] project_name = project_entity["name"] - version = get_last_version_by_subset_name( + version_entity = ayon_api.get_last_version_by_product_name( project_name, product_name, - asset_name=asset_name + folder_entity["id"] ) - if not version: + if not version_entity: self.log.warning( "No last version found, re-render not possible" ) return - representations = get_representations( - project_name, version_ids=[version["_id"]] + representations = ayon_api.get_representations( + project_name, version_ids={version_entity["id"]} ) published_files = [] for repre in representations: - if repre["context"]["family"] not in self.families: + # TODO get product type from product entity instead of + # representation 'context' data. + repre_context = repre["context"] + product_type = repre_context.get("product", {}).get("type") + if not product_type: + product_type = repre_context.get("family") + if product_type not in self.families: continue for file_info in repre.get("files"): @@ -73,7 +76,7 @@ class CollectFramesFixDef( instance.data["last_version_published_files"])) if self.rewrite_version_enable and rewrite_version: - instance.data["version"] = version["name"] + instance.data["version"] = version_entity["version"] # limits triggering version validator instance.data.pop("latestVersion") diff --git a/client/ayon_core/plugins/publish/collect_from_create_context.py b/client/ayon_core/plugins/publish/collect_from_create_context.py index 8218806c4c..b99866fed9 100644 --- a/client/ayon_core/plugins/publish/collect_from_create_context.py +++ b/client/ayon_core/plugins/publish/collect_from_create_context.py @@ -53,11 +53,11 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): context.data.update(create_context.context_data_to_store()) context.data["newPublishing"] = True # Update context data - asset_name = create_context.get_current_asset_name() + folder_path = create_context.get_current_folder_path() task_name = create_context.get_current_task_name() for key, value in ( ("AYON_PROJECT_NAME", project_name), - ("AYON_FOLDER_PATH", asset_name), + ("AYON_FOLDER_PATH", folder_path), ("AYON_TASK_NAME", task_name) ): if value is None: diff --git a/client/ayon_core/plugins/publish/collect_hierarchy.py b/client/ayon_core/plugins/publish/collect_hierarchy.py index 8ba83d582f..7387b1865b 100644 --- a/client/ayon_core/plugins/publish/collect_hierarchy.py +++ b/client/ayon_core/plugins/publish/collect_hierarchy.py @@ -21,7 +21,7 @@ class CollectHierarchy(pyblish.api.ContextPlugin): project_name = context.data["projectName"] final_context = {} final_context[project_name] = {} - final_context[project_name]['entity_type'] = 'Project' + final_context[project_name]["entity_type"] = "project" for instance in context: self.log.debug("Processing instance: `{}` ...".format(instance)) @@ -41,11 +41,6 @@ class CollectHierarchy(pyblish.api.ContextPlugin): if not instance.data.get("heroTrack"): continue - # get asset build data if any available - shot_data["inputs"] = [ - x["_id"] for x in instance.data.get("assetbuilds", []) - ] - # suppose that all instances are Shots shot_data['entity_type'] = 'Shot' shot_data['tasks'] = instance.data.get("tasks") or {} diff --git a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py index 6caee1be6a..770f3470c6 100644 --- a/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py +++ b/client/ayon_core/plugins/publish/collect_input_representations_to_versions.py @@ -1,7 +1,6 @@ +import ayon_api import pyblish.api -from ayon_core.client import get_representations - class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): """Converts collected input representations to input versions. @@ -24,14 +23,14 @@ class CollectInputRepresentationsToVersions(pyblish.api.ContextPlugin): if inst_repre: representations.update(inst_repre) - representations_docs = get_representations( - project_name=context.data["projectEntity"]["name"], + repre_entities = ayon_api.get_representations( + project_name=context.data["projectName"], representation_ids=representations, - fields=["_id", "parent"]) + fields={"id", "versionId"}) representation_id_to_version_id = { - str(repre["_id"]): repre["parent"] - for repre in representations_docs + repre["id"]: repre["versionId"] + for repre in repre_entities } for instance in context: diff --git a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py index c1326f164d..1267c009e7 100644 --- a/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py +++ b/client/ayon_core/plugins/publish/collect_scene_loaded_versions.py @@ -1,6 +1,6 @@ +import ayon_api import pyblish.api -from ayon_core.client import get_representations from ayon_core.pipeline import registered_host @@ -42,22 +42,22 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): } project_name = context.data["projectName"] - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_ids=repre_ids, - fields=["_id", "parent"] + fields={"id", "versionId"} ) - repre_doc_by_str_id = { - str(doc["_id"]): doc - for doc in repre_docs + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in repre_entities } # QUESTION should we add same representation id when loaded multiple # times? for con in containers: repre_id = con["representation"] - repre_doc = repre_doc_by_str_id.get(repre_id) - if repre_doc is None: + repre_entity = repre_entities_by_id.get(repre_id) + if repre_entity is None: self.log.warning(( "Skipping container," " did not find representation document. {}" @@ -68,8 +68,8 @@ class CollectSceneLoadedVersions(pyblish.api.ContextPlugin): # may have more then one representation that are same version version = { "container_name": con["name"], - "representation_id": repre_doc["_id"], - "version_id": repre_doc["parent"], + "representation_id": repre_entity["id"], + "version_id": repre_entity["versionId"], } loaded_versions.append(version) diff --git a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py index 7ceaf7d2ad..59a15af299 100644 --- a/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py +++ b/client/ayon_core/plugins/publish/extract_hierarchy_to_ayon.py @@ -4,12 +4,11 @@ import json import uuid import pyblish.api -from ayon_api import slugify_string +from ayon_api import slugify_string, get_folders, get_tasks from ayon_api.entity_hub import EntityHub -from ayon_core.client import get_assets, get_asset_name_identifier from ayon_core.pipeline.template_data import ( - get_asset_template_data, + get_folder_template_data, get_task_template_data, ) @@ -35,38 +34,74 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): self._fill_instance_entities(context, project_name) def _fill_instance_entities(self, context, project_name): - instances_by_asset_name = collections.defaultdict(list) + instances_by_folder_path = collections.defaultdict(list) for instance in context: if instance.data.get("publish") is False: continue - instance_entity = instance.data.get("assetEntity") + instance_entity = instance.data.get("folderEntity") if instance_entity: continue - # Skip if instance asset does not match - instance_asset_name = instance.data.get("folderPath") - instances_by_asset_name[instance_asset_name].append(instance) + folder_path = instance.data.get("folderPath") + instances_by_folder_path[folder_path].append(instance) - project_doc = context.data["projectEntity"] - asset_docs = get_assets( - project_name, asset_names=instances_by_asset_name.keys() + project_entity = context.data["projectEntity"] + folder_entities = get_folders( + project_name, folder_paths=instances_by_folder_path.keys() ) - asset_docs_by_name = { - get_asset_name_identifier(asset_doc): asset_doc - for asset_doc in asset_docs + folder_entities_by_path = { + folder_entity["path"]: folder_entity + for folder_entity in folder_entities } - for asset_name, instances in instances_by_asset_name.items(): - asset_doc = asset_docs_by_name[asset_name] - asset_data = get_asset_template_data(asset_doc, project_name) + all_task_names = set() + folder_ids = set() + # Fill folderEntity and prepare data for task entities + for folder_path, instances in instances_by_folder_path.items(): + folder_entity = folder_entities_by_path[folder_path] + folder_ids.add(folder_entity["id"]) for instance in instances: task_name = instance.data.get("task") - template_data = get_task_template_data( - project_doc, asset_doc, task_name) - template_data.update(copy.deepcopy(asset_data)) + all_task_names.add(task_name) + + # Query task entities + # Discard 'None' task names + all_task_names.discard(None) + tasks_by_name_by_folder_id = { + folder_id: {} for folder_id in folder_ids + } + task_entities = [] + if all_task_names: + task_entities = get_tasks( + project_name, + task_names=all_task_names, + folder_ids=folder_ids, + ) + for task_entity in task_entities: + task_name = task_entity["name"] + folder_id = task_entity["folderId"] + tasks_by_name_by_folder_id[folder_id][task_name] = task_entity + + for folder_path, instances in instances_by_folder_path.items(): + folder_entity = folder_entities_by_path[folder_path] + folder_id = folder_entity["id"] + folder_data = get_folder_template_data( + folder_entity, project_name + ) + task_entities_by_name = tasks_by_name_by_folder_id[folder_id] + for instance in instances: + task_name = instance.data.get("task") + task_entity = task_entities_by_name.get(task_name) + template_data = {} + if task_entity: + template_data = get_task_template_data( + project_entity, task_entity + ) + template_data.update(copy.deepcopy(folder_data)) instance.data["anatomyData"].update(template_data) - instance.data["assetEntity"] = asset_doc + instance.data["folderEntity"] = folder_entity + instance.data["taskEntity"] = task_entity def _create_hierarchy(self, context, project_name): hierarchy_context = self._filter_hierarchy(context) @@ -157,7 +192,7 @@ class ExtractHierarchyToAYON(pyblish.api.ContextPlugin): Output example: { "name": "MyProject", - "entity_type": "Project", + "entity_type": "project", "attributes": {}, "tasks": [], "children": [ diff --git a/client/ayon_core/plugins/publish/help/validate_unique_subsets.xml b/client/ayon_core/plugins/publish/help/validate_unique_subsets.xml index a4b289d848..e163fc39fe 100644 --- a/client/ayon_core/plugins/publish/help/validate_unique_subsets.xml +++ b/client/ayon_core/plugins/publish/help/validate_unique_subsets.xml @@ -1,7 +1,7 @@ -Subset not unique +Product not unique ## Clashing product names found diff --git a/client/ayon_core/plugins/publish/integrate.py b/client/ayon_core/plugins/publish/integrate.py index 12c702c93b..b7839338ae 100644 --- a/client/ayon_core/plugins/publish/integrate.py +++ b/client/ayon_core/plugins/publish/integrate.py @@ -2,27 +2,24 @@ import os import logging import sys import copy -import datetime import clique import six import pyblish.api - -from ayon_core.client.operations import ( - OperationsSession, - new_subset_document, - new_version_doc, - new_representation_doc, - prepare_subset_update_data, - prepare_version_update_data, - prepare_representation_update_data, -) - -from ayon_core.client import ( - get_representations, - get_subset_by_name, +from ayon_api import ( + get_attributes_for_type, + get_product_by_name, get_version_by_name, + get_representations, ) +from ayon_api.operations import ( + OperationsSession, + new_product_entity, + new_version_entity, + new_representation_entity, +) +from ayon_api.utils import create_entity_id + from ayon_core.lib import source_hash from ayon_core.lib.file_transaction import ( FileTransaction, @@ -36,6 +33,36 @@ from ayon_core.pipeline.publish import ( log = logging.getLogger(__name__) +def prepare_changes(old_entity, new_entity): + """Prepare changes for entity update. + + Args: + old_entity: Existing entity. + new_entity: New entity. + + Returns: + dict[str, Any]: Changes that have new entity. + + """ + changes = {} + for key in set(new_entity.keys()): + if key == "attrib": + continue + + if key in new_entity and new_entity[key] != old_entity.get(key): + changes[key] = new_entity[key] + continue + + attrib_changes = {} + if "attrib" in new_entity: + for key, value in new_entity["attrib"].items(): + if value != old_entity["attrib"].get(key): + attrib_changes[key] = value + if attrib_changes: + changes["attrib"] = attrib_changes + return changes + + def get_instance_families(instance): """Get all families of the instance""" # todo: move this to lib? @@ -164,7 +191,6 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ] def process(self, instance): - # Instance should be integrated on a farm if instance.data.get("farm"): self.log.debug( @@ -256,23 +282,22 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_name = self.get_template_name(instance) op_session = OperationsSession() - subset = self.prepare_subset( + product_entity = self.prepare_product( instance, op_session, project_name ) - version = self.prepare_version( - instance, op_session, subset, project_name + version_entity = self.prepare_version( + instance, op_session, product_entity, project_name ) - instance.data["versionEntity"] = version + instance.data["versionEntity"] = version_entity anatomy = instance.context.data["anatomy"] # Get existing representations (if any) existing_repres_by_name = { - repre_doc["name"].lower(): repre_doc - for repre_doc in get_representations( + repre_entity["name"].lower(): repre_entity + for repre_entity in get_representations( project_name, - version_ids=[version["_id"]], - fields=["_id", "name"] + version_ids=[version_entity["id"]] ) } @@ -284,7 +309,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): repre, template_name, existing_repres_by_name, - version, + version_entity, instance_stagingdir, instance) @@ -312,7 +337,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): resource_destinations.add(os.path.abspath(dst)) # Bulk write to the database - # We write the subset and version to the database before the File + # We write the product and version to the database before the File # Transaction to reduce the chances of another publish trying to # publish to the same version number since that chance can greatly # increase if the file transaction takes a long time. @@ -320,7 +345,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): self.log.info(( "Product '{}' version {} written to database.." - ).format(subset["name"], version["name"])) + ).format(product_entity["name"], version_entity["version"])) # Process all file transfers of all integrations now self.log.debug("Integrating source files to destination ...") @@ -331,58 +356,46 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "Transferred files: {}".format(file_transactions.transferred)) self.log.debug("Retrieving Representation Site Sync information ...") - # Get the accessible sites for Site Sync - addons_manager = instance.context.data["ayonAddonsManager"] - sync_server_addon = addons_manager.get("sync_server") - if sync_server_addon is None: - sites = [{ - "name": "studio", - "created_dt": datetime.datetime.now() - }] - else: - sites = sync_server_addon.compute_resource_sync_sites( - project_name=instance.data["projectEntity"]["name"] - ) - self.log.debug("Sync Server Sites: {}".format(sites)) - # Compute the resource file infos once (files belonging to the # version instance instead of an individual representation) so # we can re-use those file infos per representation - resource_file_infos = self.get_files_info(resource_destinations, - sites=sites, - anatomy=anatomy) + resource_file_infos = self.get_files_info( + resource_destinations, anatomy + ) # Finalize the representations now the published files are integrated # Get 'files' info for representations and its attached resources new_repre_names_low = set() for prepared in prepared_representations: - repre_doc = prepared["representation"] - repre_update_data = prepared["repre_doc_update_data"] + repre_entity = prepared["representation"] + repre_update_data = prepared["repre_update_data"] transfers = prepared["transfers"] destinations = [dst for src, dst in transfers] - repre_doc["files"] = self.get_files_info( - destinations, sites=sites, anatomy=anatomy + repre_files = self.get_files_info( + destinations, anatomy ) - # Add the version resource file infos to each representation - repre_doc["files"] += resource_file_infos + repre_files += resource_file_infos + repre_entity["files"] = repre_files # Set up representation for writing to the database. Since # we *might* be overwriting an existing entry if the version # already existed we'll use ReplaceOnce with `upsert=True` if repre_update_data is None: op_session.create_entity( - project_name, repre_doc["type"], repre_doc + project_name, "representation", repre_entity ) else: + # Add files to update data + repre_update_data["files"] = repre_files op_session.update_entity( project_name, - repre_doc["type"], - repre_doc["_id"], + "representation", + repre_entity["id"], repre_update_data ) - new_repre_names_low.add(repre_doc["name"].lower()) + new_repre_names_low.add(repre_entity["name"].lower()) # Delete any existing representations that didn't get any new data # if the instance is not set to append mode @@ -392,7 +405,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # We add the exact representation name because `name` is # lowercase for name matching only and not in the database op_session.delete_entity( - project_name, "representation", existing_repres["_id"] + project_name, "representation", existing_repres["id"] ) self.log.debug("{}".format(op_session.to_data())) @@ -401,7 +414,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # Backwards compatibility used in hero integration. # todo: can we avoid the need to store this? instance.data["published_representations"] = { - p["representation"]["_id"]: p for p in prepared_representations + p["representation"]["id"]: p + for p in prepared_representations } self.log.info( @@ -412,108 +426,130 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ) ) - def prepare_subset(self, instance, op_session, project_name): - asset_doc = instance.data["assetEntity"] + def prepare_product(self, instance, op_session, project_name): + folder_entity = instance.data["folderEntity"] product_name = instance.data["productName"] product_type = instance.data["productType"] self.log.debug("Product: {}".format(product_name)) - # Get existing subset if it exists - existing_subset_doc = get_subset_by_name( - project_name, product_name, asset_doc["_id"] + # Get existing product if it exists + existing_product_entity = get_product_by_name( + project_name, product_name, folder_entity["id"] ) - # Define subset data + # Define product data data = { "families": get_instance_families(instance) } + attribibutes = {} - subset_group = instance.data.get("subsetGroup") - if subset_group: - data["subsetGroup"] = subset_group - elif existing_subset_doc: - # Preserve previous subset group if new version does not set it - if "subsetGroup" in existing_subset_doc.get("data", {}): - subset_group = existing_subset_doc["data"]["subsetGroup"] - data["subsetGroup"] = subset_group + product_group = instance.data.get("productGroup") + if product_group: + attribibutes["productGroup"] = product_group + elif existing_product_entity: + # Preserve previous product group if new version does not set it + product_group = existing_product_entity.get("attrib", {}).get( + "productGroup" + ) + if product_group is not None: + attribibutes["productGroup"] = product_group - subset_id = None - if existing_subset_doc: - subset_id = existing_subset_doc["_id"] - subset_doc = new_subset_document( - product_name, product_type, asset_doc["_id"], data, subset_id + product_id = None + if existing_product_entity: + product_id = existing_product_entity["id"] + + product_entity = new_product_entity( + product_name, + product_type, + folder_entity["id"], + data=data, + attribs=attribibutes, + entity_id=product_id ) - if existing_subset_doc is None: - # Create a new subset + if existing_product_entity is None: + # Create a new product self.log.info( "Product '%s' not found, creating ..." % product_name ) op_session.create_entity( - project_name, subset_doc["type"], subset_doc + project_name, "product", product_entity ) else: - # Update existing subset data with new data and set in database. - # We also change the found subset in-place so we don't need to - # re-query the subset afterwards - subset_doc["data"].update(data) - update_data = prepare_subset_update_data( - existing_subset_doc, subset_doc + # Update existing product data with new data and set in database. + # We also change the found product in-place so we don't need to + # re-query the product afterwards + update_data = prepare_changes( + existing_product_entity, product_entity ) op_session.update_entity( project_name, - subset_doc["type"], - subset_doc["_id"], + "product", + product_entity["id"], update_data ) self.log.debug("Prepared product: {}".format(product_name)) - return subset_doc + return product_entity - def prepare_version(self, instance, op_session, subset_doc, project_name): + def prepare_version( + self, instance, op_session, product_entity, project_name + ): version_number = instance.data["version"] + task_id = None + task_entity = instance.data.get("taskEntity") + if task_entity: + task_id = task_entity["id"] existing_version = get_version_by_name( project_name, version_number, - subset_doc["_id"], - fields=["_id"] + product_entity["id"] ) version_id = None if existing_version: - version_id = existing_version["_id"] + version_id = existing_version["id"] - version_data = self.create_version_data(instance) - version_doc = new_version_doc( + all_version_data = self.create_version_data(instance) + version_data = {} + version_attributes = {} + attr_defs = self._get_attributes_for_type(instance.context, "version") + for key, value in all_version_data.items(): + if key in attr_defs: + version_attributes[key] = value + else: + version_data[key] = value + + version_entity = new_version_entity( version_number, - subset_doc["_id"], - version_data, - version_id + product_entity["id"], + task_id=task_id, + data=version_data, + attribs=version_attributes, + entity_id=version_id, ) if existing_version: self.log.debug("Updating existing version ...") - update_data = prepare_version_update_data( - existing_version, version_doc - ) + update_data = prepare_changes(existing_version, version_entity) op_session.update_entity( project_name, - version_doc["type"], - version_doc["_id"], + "version", + version_entity["id"], update_data ) else: self.log.debug("Creating new version ...") op_session.create_entity( - project_name, version_doc["type"], version_doc + project_name, "version", version_entity ) self.log.debug( - "Prepared version: v{0:03d}".format(version_doc["name"]) + "Prepared version: v{0:03d}".format(version_entity["version"]) ) - return version_doc + return version_entity def _validate_repre_files(self, files, is_sequence_representation): """Validate representation files before transfer preparation. @@ -552,13 +588,15 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ", ".join([str(rem) for rem in remainders]) )) - def prepare_representation(self, repre, - template_name, - existing_repres_by_name, - version, - instance_stagingdir, - instance): - + def prepare_representation( + self, + repre, + template_name, + existing_repres_by_name, + version_entity, + instance_stagingdir, + instance + ): # pre-flight validations if repre["ext"].startswith("."): raise KnownPublishError(( @@ -581,7 +619,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): template_data["ext"] = repre["ext"] # allow overwriting existing version - template_data["version"] = version["name"] + template_data["version"] = version_entity["version"] # add template data for colorspaceData if repre.get("colorspaceData"): @@ -823,7 +861,7 @@ class IntegrateAsset(pyblish.api.InstancePlugin): existing = existing_repres_by_name.get(repre["name"].lower()) repre_id = None if existing: - repre_id = existing["_id"] + repre_id = existing["id"] # Store first transferred destination as published path data # - used primarily for reviews that are integrated to custom modules @@ -835,25 +873,37 @@ class IntegrateAsset(pyblish.api.InstancePlugin): # todo: `repre` is not the actual `representation` entity # we should simplify/clarify difference between data above # and the actual representation entity for the database - data = repre.get("data", {}) - data.update({"path": published_path, "template": template}) + attr_defs = self._get_attributes_for_type( + instance.context, "representation" + ) + attributes = {"path": published_path, "template": template} + data = {"context": repre_context} + for key, value in repre.get("data", {}).items(): + if key in attr_defs: + attributes[key] = value + else: + data[key] = value # add colorspace data if any exists on representation if repre.get("colorspaceData"): data["colorspaceData"] = repre["colorspaceData"] - repre_doc = new_representation_doc( - repre["name"], version["_id"], repre_context, data, repre_id + repre_doc = new_representation_entity( + repre["name"], + version_entity["id"], + # files are filled afterwards + [], + data=data, + attribs=attributes, + entity_id=repre_id ) update_data = None if repre_id is not None: - update_data = prepare_representation_update_data( - existing, repre_doc - ) + update_data = prepare_changes(existing, repre_doc) return { "representation": repre_doc, - "repre_doc_update_data": update_data, + "repre_update_data": update_data, "anatomy_data": template_data, "transfers": transfers, # todo: avoid the need for 'published_files' used by Integrate Hero @@ -950,13 +1000,13 @@ class IntegrateAsset(pyblish.api.InstancePlugin): '{root}/MyProject1/Assets...' Args: - anatomy: anatomy part from instance - path: path (absolute) - Returns: - path: modified path if possible, or unmodified path - + warning logged - """ + anatomy (Anatomy): Project anatomy. + path (str): Absolute path. + Returns: + str: Path where root path is replaced by formatting string. + + """ success, rootless_path = anatomy.find_root_template_from_path(path) if success: path = rootless_path @@ -967,43 +1017,41 @@ class IntegrateAsset(pyblish.api.InstancePlugin): ).format(path)) return path - def get_files_info(self, destinations, sites, anatomy): + def get_files_info(self, filepaths, anatomy): """Prepare 'files' info portion for representations. Arguments: - destinations (list): List of transferred file destinations - sites (list): array of published locations - anatomy: anatomy part from instance - Returns: - output_resources: array of dictionaries to be added to 'files' key - in representation - """ + filepaths (Iterable[str]): List of transferred file paths. + anatomy (Anatomy): Project anatomy. + Returns: + list[dict[str, Any]]: Representation 'files' information. + + """ file_infos = [] - for file_path in destinations: - file_info = self.prepare_file_info(file_path, anatomy, sites=sites) + for filepath in filepaths: + file_info = self.prepare_file_info(filepath, anatomy) file_infos.append(file_info) return file_infos - def prepare_file_info(self, path, anatomy, sites): + def prepare_file_info(self, path, anatomy): """ Prepare information for one file (asset or resource) Arguments: - path: destination url of published file - anatomy: anatomy part from instance - sites: array of published locations, - [ {'name':'studio', 'created_dt':date} by default - keys expected ['studio', 'site1', 'gdrive1'] + path (str): Destination url of published file. + anatomy (Anatomy): Project anatomy part from instance. Returns: - dict: file info dictionary - """ + dict[str, Any]: Representation file info dictionary. + """ return { + "id": create_entity_id(), + "name": os.path.basename(path), "path": self.get_rootless_path(anatomy, path), "size": os.path.getsize(path), "hash": source_hash(path), - "sites": sites + "hash_type": "op3", } def _validate_path_in_project_roots(self, anatomy, file_path): @@ -1012,10 +1060,11 @@ class IntegrateAsset(pyblish.api.InstancePlugin): Used to check that published path belongs to project, eg. we are not trying to publish to local only folder. Args: - anatomy (Anatomy) - file_path (str) - Raises - (KnownPublishError) + anatomy (Anatomy): Project anatomy. + file_path (str): Filepath. + + Raises: + KnownPublishError: When failed to find root for the path. """ path = self.get_rootless_path(anatomy, file_path) if not path: @@ -1023,3 +1072,21 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "Destination path '{}' ".format(file_path) + "must be in project dir" )) + + def _get_attributes_for_type(self, context, entity_type): + return self._get_attributes_by_type(context)[entity_type] + + def _get_attributes_by_type(self, context): + attributes = context.data.get("ayonAttributes") + if attributes is None: + attributes = {} + for key in ( + "project", + "folder", + "product", + "version", + "representation", + ): + attributes[key] = get_attributes_for_type(key) + context.data["ayonAttributes"] = attributes + return attributes diff --git a/client/ayon_core/plugins/publish/integrate_hero_version.py b/client/ayon_core/plugins/publish/integrate_hero_version.py index c275f75118..6d690ba94b 100644 --- a/client/ayon_core/plugins/publish/integrate_hero_version.py +++ b/client/ayon_core/plugins/publish/integrate_hero_version.py @@ -1,30 +1,51 @@ import os import copy -import clique import errno import shutil +import clique import pyblish.api - -from ayon_core.client import ( - get_version_by_id, - get_hero_version_by_subset_id, - get_archived_representations, - get_representations, -) -from ayon_core.client.operations import ( +import ayon_api +from ayon_api.operations import ( OperationsSession, - new_hero_version_doc, - prepare_hero_version_update_data, - prepare_representation_update_data, -) -from ayon_core.lib import create_hard_link -from ayon_core.pipeline import ( - schema + new_version_entity, ) +from ayon_api.utils import create_entity_id + +from ayon_core.lib import create_hard_link, source_hash from ayon_core.pipeline.publish import get_publish_template_name +def prepare_changes(old_entity, new_entity): + """Prepare changes for entity update. + + Args: + old_entity: Existing entity. + new_entity: New entity. + + Returns: + dict[str, Any]: Changes that have new entity. + + """ + changes = {} + for key in set(new_entity.keys()): + if key == "attrib": + continue + + if key in new_entity and new_entity[key] != old_entity.get(key): + changes[key] = new_entity[key] + continue + + attrib_changes = {} + if "attrib" in new_entity: + for key, value in new_entity["attrib"].items(): + if value != old_entity["attrib"].get(key): + attrib_changes[key] = value + if attrib_changes: + changes["attrib"] = attrib_changes + return changes + + class IntegrateHeroVersion(pyblish.api.InstancePlugin): label = "Integrate Hero Version" # Must happen after IntegrateNew @@ -150,7 +171,7 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): )) return - if src_version_entity["name"] == 0: + if src_version_entity["version"] == 0: self.log.debug( "Version 0 cannot have hero version. Skipping." ) @@ -200,39 +221,45 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): old_version, old_repres = self.current_hero_ents( project_name, src_version_entity ) - - old_repres_by_name = { - repre["name"].lower(): repre for repre in old_repres - } + inactive_old_repres_by_name = {} + old_repres_by_name = {} + for repre in old_repres: + low_name = repre["name"].lower() + if repre["active"]: + old_repres_by_name[low_name] = repre + else: + inactive_old_repres_by_name[low_name] = repre op_session = OperationsSession() entity_id = None if old_version: - entity_id = old_version["_id"] + entity_id = old_version["id"] - new_hero_version = new_hero_version_doc( - src_version_entity["parent"], - copy.deepcopy(src_version_entity["data"]), - src_version_entity["name"], - entity_id=entity_id + new_hero_version = new_version_entity( + - src_version_entity["version"], + src_version_entity["productId"], + task_id=src_version_entity.get("taskId"), + data=copy.deepcopy(src_version_entity["data"]), + attribs=copy.deepcopy(src_version_entity["attrib"]), + entity_id=entity_id, ) if old_version: self.log.debug("Replacing old hero version.") - update_data = prepare_hero_version_update_data( + update_data = prepare_changes( old_version, new_hero_version ) op_session.update_entity( project_name, - new_hero_version["type"], - old_version["_id"], + "version", + old_version["id"], update_data ) else: self.log.debug("Creating first hero version.") op_session.create_entity( - project_name, new_hero_version["type"], new_hero_version + project_name, "version", new_hero_version ) # Separate old representations into `to replace` and `to delete` @@ -249,16 +276,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): if old_repres_by_name: old_repres_to_delete = old_repres_by_name - archived_repres = list(get_archived_representations( - project_name, - # Check what is type of archived representation - version_ids=[new_hero_version["_id"]] - )) - archived_repres_by_name = {} - for repre in archived_repres: - repre_name_low = repre["name"].lower() - archived_repres_by_name[repre_name_low] = repre - backup_hero_publish_dir = None if os.path.exists(hero_publish_dir): backup_hero_publish_dir = hero_publish_dir + ".BACKUP" @@ -306,8 +323,10 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): "Could not create hero version because it is not" " possible to replace current hero files." )) + try: src_to_dst_file_paths = [] + repre_integrate_data = [] path_template_obj = anatomy.templates_obj[template_key]["path"] for repre_info in published_repres.values(): @@ -322,10 +341,6 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): # Get filled path to repre context template_filled = path_template_obj.format_strict(anatomy_data) - repre_data = { - "path": str(template_filled), - "template": hero_template - } repre_context = template_filled.used_values for key in self.db_representation_context_keys: value = anatomy_data.get(key) @@ -333,14 +348,19 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): repre_context[key] = value # Prepare new repre - repre = copy.deepcopy(repre_info["representation"]) - repre["parent"] = new_hero_version["_id"] - repre["context"] = repre_context - repre["data"] = repre_data - repre.pop("_id", None) + repre_entity = copy.deepcopy(repre_info["representation"]) + repre_entity.pop("id", None) + repre_entity["versionId"] = new_hero_version["id"] + repre_entity["context"] = repre_context + repre_entity["attrib"] = { + "path": str(template_filled), + "template": hero_template + } + dst_paths = [] # Prepare paths of source and destination files if len(published_files) == 1: + dst_paths.append(str(template_filled)) src_to_dst_file_paths.append( (published_files[0], template_filled) ) @@ -376,84 +396,11 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): src_to_dst_file_paths.append( (src_file, dst_file) ) + dst_paths.append(dst_file) - # replace original file name with hero name in repre doc - for index in range(len(repre.get("files"))): - file = repre.get("files")[index] - file_name = os.path.basename(file.get('path')) - for src_file, dst_file in src_to_dst_file_paths: - src_file_name = os.path.basename(src_file) - if src_file_name == file_name: - repre["files"][index]["path"] = self._update_path( - anatomy, repre["files"][index]["path"], - src_file, dst_file) - - repre["files"][index]["hash"] = self._update_hash( - repre["files"][index]["hash"], - src_file_name, dst_file - ) - - schema.validate(repre) - - repre_name_low = repre["name"].lower() - # Replace current representation - if repre_name_low in old_repres_to_replace: - old_repre = old_repres_to_replace.pop(repre_name_low) - - repre["_id"] = old_repre["_id"] - update_data = prepare_representation_update_data( - old_repre, repre) - - # Keep previously synchronized sites up-to-date - # by comparing old and new sites and adding old sites - # if missing in new ones - # Prepare all sites from all files in old representation - old_site_names = set() - for file_info in old_repre.get("files", []): - old_site_names |= { - site["name"] - for site in file_info["sites"] - } - - for file_info in update_data.get("files", []): - file_info.setdefault("sites", []) - file_info_site_names = { - site["name"] - for site in file_info["sites"] - } - for site_name in old_site_names: - if site_name not in file_info_site_names: - file_info["sites"].append({ - "name": site_name - }) - - op_session.update_entity( - project_name, - old_repre["type"], - old_repre["_id"], - update_data - ) - - # Unarchive representation - elif repre_name_low in archived_repres_by_name: - archived_repre = archived_repres_by_name.pop( - repre_name_low - ) - repre["_id"] = archived_repre["old_id"] - update_data = prepare_representation_update_data( - archived_repre, repre) - op_session.update_entity( - project_name, - old_repre["type"], - archived_repre["_id"], - update_data - ) - - # Create representation - else: - repre.pop("_id", None) - op_session.create_entity(project_name, "representation", - repre) + repre_integrate_data.append( + (repre_entity, dst_paths) + ) self.path_checks = [] @@ -466,28 +413,61 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): for src_path, dst_path in other_file_paths_mapping: self.copy_file(src_path, dst_path) - # Archive not replaced old representations - for repre_name_low, repre in old_repres_to_delete.items(): - # Replace archived representation (This is backup) - # - should not happen to have both repre and archived repre - if repre_name_low in archived_repres_by_name: - archived_repre = archived_repres_by_name.pop( - repre_name_low + # Update prepared representation etity data with files + # and integrate it to server. + # NOTE: This must happen with existing files on disk because of + # file hash. + for repre_entity, dst_paths in repre_integrate_data: + repre_files = self.get_files_info(dst_paths, anatomy) + repre_entity["files"] = repre_files + + repre_name_low = repre_entity["name"].lower() + # Replace current representation + if repre_name_low in old_repres_to_replace: + old_repre = old_repres_to_replace.pop(repre_name_low) + + repre_entity["id"] = old_repre["id"] + update_data = prepare_changes(old_repre, repre_entity) + + op_session.update_entity( + project_name, + "representation", + old_repre["id"], + update_data ) - changes = {"old_id": repre["_id"], - "_id": archived_repre["_id"], - "type": archived_repre["type"]} - op_session.update_entity(project_name, - archived_repre["type"], - archived_repre["_id"], - changes) + # Activate representation + elif repre_name_low in inactive_old_repres_by_name: + inactive_repre = inactive_old_repres_by_name.pop( + repre_name_low + ) + repre_entity["id"] = inactive_repre["id"] + update_data = prepare_changes( + inactive_repre, repre_entity + ) + op_session.update_entity( + project_name, + "representation", + inactive_repre["id"], + update_data + ) + + # Create representation else: - repre["old_id"] = repre.pop("_id") - repre["type"] = "archived_representation" - op_session.create_entity(project_name, - "archived_representation", - repre) + op_session.create_entity( + project_name, + "representation", + repre_entity + ) + + # Deactivate not replaced representations + for repre in old_repres_to_delete.values(): + op_session.update_entity( + project_name, + "representation", + repre["id"], + {"active": False} + ) op_session.commit() @@ -519,13 +499,42 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): instance.data["productName"] )) - def get_all_files_from_path(self, path): - files = [] - for (dir_path, dir_names, file_names) in os.walk(path): - for file_name in file_names: - _path = os.path.join(dir_path, file_name) - files.append(_path) - return files + def get_files_info(self, filepaths, anatomy): + """Prepare 'files' info portion for representations. + + Arguments: + filepaths (Iterable[str]): List of transferred file paths. + anatomy (Anatomy): Project anatomy. + + Returns: + list[dict[str, Any]]: Representation 'files' information. + + """ + file_infos = [] + for filepath in filepaths: + file_info = self.prepare_file_info(filepath, anatomy) + file_infos.append(file_info) + return file_infos + + def prepare_file_info(self, path, anatomy): + """ Prepare information for one file (asset or resource) + + Arguments: + path (str): Destination url of published file. + anatomy (Anatomy): Project anatomy part from instance. + + Returns: + dict[str, Any]: Representation file info dictionary. + + """ + return { + "id": create_entity_id(), + "name": os.path.basename(path), + "path": self.get_rootless_path(anatomy, path), + "size": os.path.getsize(path), + "hash": source_hash(path), + "hash_type": "op3", + } def get_publish_dir(self, instance, template_key): anatomy = instance.context.data["anatomy"] @@ -581,6 +590,33 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): logger=self.log ) + def get_rootless_path(self, anatomy, path): + """Returns, if possible, path without absolute portion from root + (eg. 'c:\' or '/opt/..') + + This information is platform dependent and shouldn't be captured. + Example: + 'c:/projects/MyProject1/Assets/publish...' > + '{root}/MyProject1/Assets...' + + Args: + anatomy (Anatomy): Project anatomy. + path (str): Absolute path. + + Returns: + str: Path where root path is replaced by formatting string. + + """ + success, rootless_path = anatomy.find_root_template_from_path(path) + if success: + path = rootless_path + else: + self.log.warning(( + "Could not find root path for remapping \"{}\"." + " This may cause issues on farm." + ).format(path)) + return path + def copy_file(self, src_path, dst_path): # TODO check drives if are the same to check if cas hardlink dirname = os.path.dirname(dst_path) @@ -617,48 +653,25 @@ class IntegrateHeroVersion(pyblish.api.InstancePlugin): def version_from_representations(self, project_name, repres): for repre in repres: - version = get_version_by_id(project_name, repre["parent"]) + version = ayon_api.get_version_by_id( + project_name, repre["versionId"] + ) if version: return version def current_hero_ents(self, project_name, version): - hero_version = get_hero_version_by_subset_id( - project_name, version["parent"] + hero_version = ayon_api.get_hero_version_by_product_id( + project_name, version["productId"] ) if not hero_version: return (None, []) - hero_repres = list(get_representations( - project_name, version_ids=[hero_version["_id"]] + hero_repres = list(ayon_api.get_representations( + project_name, version_ids={hero_version["id"]} )) return (hero_version, hero_repres) - def _update_path(self, anatomy, path, src_file, dst_file): - """ - Replaces source path with new hero path - - 'path' contains original path with version, must be replaced with - 'hero' path (with 'hero' label and without version) - - Args: - anatomy (Anatomy) - to get rootless style of path - path (string) - path from DB - src_file (string) - original file path - dst_file (string) - hero file path - """ - _, rootless = anatomy.find_root_template_from_path(dst_file) - _, rtls_src = anatomy.find_root_template_from_path(src_file) - return path.replace(rtls_src, rootless) - - def _update_hash(self, hash, src_file_name, dst_file): - """ - Updates hash value with proper hero name - """ - src_file_name = self._get_name_without_ext(src_file_name) - hero_file_name = self._get_name_without_ext(dst_file) - return hash.replace(src_file_name, hero_file_name) - def _get_name_without_ext(self, value): file_name = os.path.basename(value) file_name, _ = os.path.splitext(file_name) diff --git a/client/ayon_core/plugins/publish/integrate_inputlinks.py b/client/ayon_core/plugins/publish/integrate_inputlinks.py index f7e802f410..16aef09a39 100644 --- a/client/ayon_core/plugins/publish/integrate_inputlinks.py +++ b/client/ayon_core/plugins/publish/integrate_inputlinks.py @@ -55,8 +55,7 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): if not instance.data.get("publish", True): continue - version_doc = instance.data.get("versionEntity") - if not version_doc: + if not instance.data.get("versionEntity"): self.log.debug( "Instance {} doesn't have version.".format(instance)) continue @@ -88,14 +87,14 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): self.log.warn("No workfile in this publish session.") return - workfile_version_id = workfile_instance.data["versionEntity"]["_id"] + workfile_version_id = workfile_instance.data["versionEntity"]["id"] # link workfile to all publishing versions for instance in other_instances: self.add_link( new_links_by_type, "generative", workfile_version_id, - instance.data["versionEntity"]["_id"], + instance.data["versionEntity"]["id"], ) loaded_versions = workfile_instance.context.get("loadedVersions") @@ -123,7 +122,7 @@ class IntegrateInputLinksAYON(pyblish.api.ContextPlugin): new_links_by_type, "generative", input_version, - version_entity["_id"], + version_entity["id"], ) def _get_existing_links(self, project_name, link_type, entity_ids): diff --git a/client/ayon_core/plugins/publish/integrate_product_group.py b/client/ayon_core/plugins/publish/integrate_product_group.py index f69e7744d9..90887a359d 100644 --- a/client/ayon_core/plugins/publish/integrate_product_group.py +++ b/client/ayon_core/plugins/publish/integrate_product_group.py @@ -1,10 +1,10 @@ -"""Produces instance.data["subsetGroup"] data used during integration. +"""Produces instance.data["productGroup"] data used during integration. Requires: dict -> context["anatomyData"] *(pyblish.api.CollectorOrder + 0.49) Provides: - instance -> subsetGroup (str) + instance -> productGroup (str) """ import pyblish.api @@ -18,7 +18,7 @@ from ayon_core.lib import ( class IntegrateProductGroup(pyblish.api.InstancePlugin): - """Integrate Subset Group for publish.""" + """Integrate Product Group for publish.""" # Run after CollectAnatomyInstanceData order = pyblish.api.IntegratorOrder - 0.1 @@ -37,11 +37,11 @@ class IntegrateProductGroup(pyblish.api.InstancePlugin): if not self.product_grouping_profiles: return - if instance.data.get("subsetGroup"): - # If subsetGroup is already set then allow that value to remain + if instance.data.get("productGroup"): + # If productGroup is already set then allow that value to remain self.log.debug(( "Skipping collect product group due to existing value: {}" - ).format(instance.data["subsetGroup"])) + ).format(instance.data["productGroup"])) return # Skip if there is no matching profile @@ -79,11 +79,11 @@ class IntegrateProductGroup(pyblish.api.InstancePlugin): except (KeyError, TemplateUnsolved): keys = fill_pairs.keys() self.log.warning(( - "Subset grouping failed. Only {} are expected in Settings" + "Product grouping failed. Only {} are expected in Settings" ).format(','.join(keys))) if filled_template: - instance.data["subsetGroup"] = filled_template + instance.data["productGroup"] = filled_template def get_profile_filter_criteria(self, instance): """Return filter criteria for `filter_profiles`""" diff --git a/client/ayon_core/plugins/publish/integrate_thumbnail.py b/client/ayon_core/plugins/publish/integrate_thumbnail.py index 9eb649d5a0..ca32e60cc2 100644 --- a/client/ayon_core/plugins/publish/integrate_thumbnail.py +++ b/client/ayon_core/plugins/publish/integrate_thumbnail.py @@ -26,9 +26,8 @@ import os import collections import pyblish.api - -from ayon_core.client import get_versions -from ayon_core.client.operations import OperationsSession +import ayon_api +from ayon_api.operations import OperationsSession InstanceFilterResult = collections.namedtuple( "InstanceFilterResult", @@ -59,20 +58,20 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): for instance_items in filtered_instance_items } # Query versions - version_docs = get_versions( + version_entities = ayon_api.get_versions( project_name, version_ids=version_ids, hero=True, - fields=["_id", "type", "name"] + fields={"id", "version"} ) # Store version by their id (converted to string) - version_docs_by_str_id = { - str(version_doc["_id"]): version_doc - for version_doc in version_docs + version_entities_by_id = { + version_entity["id"]: version_entity + for version_entity in version_entities } self._integrate_thumbnails( filtered_instance_items, - version_docs_by_str_id, + version_entities_by_id, project_name ) @@ -84,6 +83,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): ) filtered_instances = [] + anatomy = context.data["anatomy"] for instance in context: instance_label = self._get_instance_label(instance) # Skip instances without published representations @@ -99,7 +99,9 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): # Find thumbnail path on instance thumbnail_path = ( instance.data.get("thumbnailPath") - or self._get_instance_thumbnail_path(published_repres) + or self._get_instance_thumbnail_path( + published_repres, anatomy + ) ) if thumbnail_path: self.log.debug(( @@ -132,70 +134,72 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): def _get_version_id(self, published_representations): for repre_info in published_representations.values(): - return repre_info["representation"]["parent"] + return repre_info["representation"]["versionId"] - def _get_instance_thumbnail_path(self, published_representations): - thumb_repre_doc = None + def _get_instance_thumbnail_path( + self, published_representations, anatomy + ): + thumb_repre_entity = None for repre_info in published_representations.values(): - repre_doc = repre_info["representation"] - if "thumbnail" in repre_doc["name"].lower(): - thumb_repre_doc = repre_doc + repre_entity = repre_info["representation"] + if "thumbnail" in repre_entity["name"].lower(): + thumb_repre_entity = repre_entity break - if thumb_repre_doc is None: + if thumb_repre_entity is None: self.log.debug( "There is no representation with name \"thumbnail\"" ) return None - path = thumb_repre_doc["data"]["path"] - if not os.path.exists(path): + path = thumb_repre_entity["attrib"]["path"] + filled_path = anatomy.fill_root(path) + if not os.path.exists(filled_path): self.log.warning( - "Thumbnail file cannot be found. Path: {}".format(path) + "Thumbnail file cannot be found. Path: {}".format(filled_path) ) return None - return os.path.normpath(path) + return os.path.normpath(filled_path) def _integrate_thumbnails( self, filtered_instance_items, - version_docs_by_str_id, + version_entities_by_id, project_name ): - from ayon_core.client.operations import create_thumbnail - # Make sure each entity id has defined only one thumbnail id thumbnail_info_by_entity_id = {} for instance_item in filtered_instance_items: instance, thumbnail_path, version_id = instance_item instance_label = self._get_instance_label(instance) - version_doc = version_docs_by_str_id.get(version_id) - if not version_doc: + version_entity = version_entities_by_id.get(version_id) + if not version_entity: self.log.warning(( "Version entity for instance \"{}\" was not found." ).format(instance_label)) continue - thumbnail_id = create_thumbnail(project_name, thumbnail_path) + thumbnail_id = ayon_api.create_thumbnail( + project_name, thumbnail_path + ) # Set thumbnail id for version thumbnail_info_by_entity_id[version_id] = { "thumbnail_id": thumbnail_id, - "entity_type": version_doc["type"], + "entity_type": "version", } - if version_doc["type"] == "hero_version": + version_name = version_entity["version"] + if version_name < 0: version_name = "Hero" - else: - version_name = version_doc["name"] self.log.debug("Setting thumbnail for version \"{}\" <{}>".format( version_name, version_id )) - asset_entity = instance.data["assetEntity"] + folder_id = instance.data["folderEntity"]["id"] folder_path = instance.data["folderPath"] - thumbnail_info_by_entity_id[asset_entity["_id"]] = { + thumbnail_info_by_entity_id[folder_id] = { "thumbnail_id": thumbnail_id, - "entity_type": "asset", + "entity_type": "folder", } self.log.debug("Setting thumbnail for folder \"{}\" <{}>".format( folder_path, version_id @@ -208,7 +212,7 @@ class IntegrateThumbnailsAYON(pyblish.api.ContextPlugin): project_name, thumbnail_info["entity_type"], entity_id, - {"data.thumbnail_id": thumbnail_id} + {"thumbnailId": thumbnail_id} ) op_session.commit() diff --git a/client/ayon_core/plugins/publish/integrate_version_attrs.py b/client/ayon_core/plugins/publish/integrate_version_attrs.py index bc09af9db0..eadf4a194c 100644 --- a/client/ayon_core/plugins/publish/integrate_version_attrs.py +++ b/client/ayon_core/plugins/publish/integrate_version_attrs.py @@ -1,7 +1,6 @@ import pyblish.api import ayon_api - -from ayon_core.client.operations import OperationsSession +from ayon_api.operations import OperationsSession class IntegrateVersionAttributes(pyblish.api.ContextPlugin): @@ -33,6 +32,8 @@ class IntegrateVersionAttributes(pyblish.api.ContextPlugin): version_entity = instance.data.get("versionEntity") if not version_entity: continue + + current_attributes = version_entity["attrib"] attributes = instance.data.get("versionAttributes") if not attributes: self.log.debug(( @@ -45,7 +46,7 @@ class IntegrateVersionAttributes(pyblish.api.ContextPlugin): for attr, value in attributes.items(): if attr not in available_attributes: skipped_attributes.add(attr) - else: + elif current_attributes.get(attr) != value: filtered_attributes[attr] = value if not filtered_attributes: @@ -56,12 +57,12 @@ class IntegrateVersionAttributes(pyblish.api.ContextPlugin): continue self.log.debug("Updating attributes on version {} to {}".format( - version_entity["_id"], str(filtered_attributes) + version_entity["id"], str(filtered_attributes) )) op_session.update_entity( project_name, "version", - version_entity["_id"], + version_entity["id"], {"attrib": filtered_attributes} ) diff --git a/client/ayon_core/plugins/publish/validate_asset_docs.py b/client/ayon_core/plugins/publish/validate_asset_docs.py index 22d957f6e2..95fe4252be 100644 --- a/client/ayon_core/plugins/publish/validate_asset_docs.py +++ b/client/ayon_core/plugins/publish/validate_asset_docs.py @@ -2,27 +2,27 @@ import pyblish.api from ayon_core.pipeline import PublishValidationError -class ValidateAssetDocs(pyblish.api.InstancePlugin): - """Validate existence of asset documents on instances. +class ValidateFolderEntities(pyblish.api.InstancePlugin): + """Validate existence of folder entity on instances. - Without asset document it is not possible to publish the instance. + Without folder entity it is not possible to publish the instance. - If context has set asset document the validation is skipped. + If context has set folder entity the validation is skipped. - Plugin was added because there are cases when context asset is not defined - e.g. in tray publisher. + Plugin was added because there are cases when context folder is not + defined e.g. in tray publisher. """ - label = "Validate Asset docs" + label = "Validate Folder entities" order = pyblish.api.ValidatorOrder def process(self, instance): - context_asset_doc = instance.context.data.get("assetEntity") - if context_asset_doc: + context_folder_entity = instance.context.data.get("folderEntity") + if context_folder_entity: return - if instance.data.get("assetEntity"): - self.log.debug("Instance has set asset document in its data.") + if instance.data.get("folderEntity"): + self.log.debug("Instance has set fodler entity in its data.") elif instance.data.get("newAssetPublishing"): # skip if it is editorial @@ -30,6 +30,6 @@ class ValidateAssetDocs(pyblish.api.InstancePlugin): else: raise PublishValidationError(( - "Instance \"{}\" doesn't have asset document " + "Instance \"{}\" doesn't have folder entity " "set which is needed for publishing." ).format(instance.data["name"])) diff --git a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py index dd1a19f602..9b6794a0c4 100644 --- a/client/ayon_core/plugins/publish/validate_editorial_asset_name.py +++ b/client/ayon_core/plugins/publish/validate_editorial_asset_name.py @@ -1,19 +1,20 @@ from pprint import pformat +import ayon_api import pyblish.api -from ayon_core.client import get_assets, get_asset_name_identifier +from ayon_core.pipeline import KnownPublishError class ValidateEditorialAssetName(pyblish.api.ContextPlugin): - """ Validating if editorial's asset names are not already created in db. + """ Validating if editorial's folder names are not already created in db. Checking variations of names with different size of caps or with or without underscores. """ order = pyblish.api.ValidatorOrder - label = "Validate Editorial Asset Name" + label = "Validate Editorial Folder Name" hosts = [ "hiero", "resolve", @@ -23,62 +24,67 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): def process(self, context): - asset_and_parents = self.get_parents(context) - self.log.debug("__ asset_and_parents: {}".format(asset_and_parents)) + folder_and_parents = self.get_parents(context) + self.log.debug("__ folder_and_parents: {}".format(folder_and_parents)) project_name = context.data["projectName"] - db_assets = list(get_assets( - project_name, fields=["name", "data.parents"] + folder_entities = list(ayon_api.get_folders( + project_name, fields={"path"} )) - self.log.debug("__ db_assets: {}".format(db_assets)) + self.log.debug("__ folder_entities: {}".format(folder_entities)) - asset_db_docs = { - get_asset_name_identifier(asset_doc): list( - asset_doc["data"]["parents"] + existing_folder_paths = { + folder_entity["path"]: ( + folder_entity["path"].lstrip("/").rsplit("/")[0] ) - for asset_doc in db_assets + for folder_entity in folder_entities } self.log.debug("__ project_entities: {}".format( - pformat(asset_db_docs))) + pformat(existing_folder_paths))) - assets_missing_name = {} - assets_wrong_parent = {} - for asset in asset_and_parents.keys(): - if asset not in asset_db_docs.keys(): + folders_missing_name = {} + folders_wrong_parent = {} + for folder_path in folder_and_parents.keys(): + if folder_path not in existing_folder_paths.keys(): # add to some nonexistent list for next layer of check - assets_missing_name[asset] = asset_and_parents[asset] + folders_missing_name[folder_path] = ( + folder_and_parents[folder_path] + ) continue - if asset_and_parents[asset] != asset_db_docs[asset]: + existing_parents = existing_folder_paths[folder_path] + if folder_and_parents[folder_path] != existing_parents: # add to some nonexistent list for next layer of check - assets_wrong_parent[asset] = { - "required": asset_and_parents[asset], - "already_in_db": asset_db_docs[asset] + folders_wrong_parent[folder_path] = { + "required": folder_and_parents[folder_path], + "already_in_db": existing_folder_paths[folder_path] } continue - self.log.debug("correct asset: {}".format(asset)) + self.log.debug("correct folder: {}".format(folder_path)) - if assets_missing_name: + if folders_missing_name: wrong_names = {} self.log.debug( - ">> assets_missing_name: {}".format(assets_missing_name)) + ">> folders_missing_name: {}".format(folders_missing_name)) - # This will create set asset names - asset_names = { - a.lower().replace("_", "") for a in asset_db_docs + # This will create set of folder paths + folder_paths = { + folder_path.lower().replace("_", "") + for folder_path in existing_folder_paths } - for asset in assets_missing_name: - _asset = asset.lower().replace("_", "") - if _asset in asset_names: - wrong_names[asset].update( + for folder_path in folders_missing_name: + _folder_path = folder_path.lower().replace("_", "") + if _folder_path in folder_paths: + wrong_names[folder_path].update( { - "required_name": asset, + "required_name": folder_path, "used_variants_in_db": [ - a for a in asset_db_docs - if a.lower().replace("_", "") == _asset + p + for p in existing_folder_paths + if p.lower().replace("_", "") == _folder_path ] } ) @@ -87,33 +93,19 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): self.log.debug( ">> wrong_names: {}".format(wrong_names)) raise Exception( - "Some already existing asset name variants `{}`".format( + "Some already existing folder name variants `{}`".format( wrong_names)) - if assets_wrong_parent: + if folders_wrong_parent: self.log.debug( - ">> assets_wrong_parent: {}".format(assets_wrong_parent)) - raise Exception( - "Wrong parents on assets `{}`".format(assets_wrong_parent)) - - def _get_all_assets(self, input_dict): - """ Returns asset names in list. - - List contains all asset names including parents - """ - for key in input_dict.keys(): - # check if child key is available - if input_dict[key].get("childs"): - # loop deeper - self._get_all_assets( - input_dict[key]["childs"]) - else: - self.all_testing_assets.append(key) + ">> folders_wrong_parent: {}".format(folders_wrong_parent)) + raise KnownPublishError( + "Wrong parents on folders `{}`".format(folders_wrong_parent)) def get_parents(self, context): - return_dict = {} + output = {} for instance in context: - asset = instance.data["folderPath"] + folder_path = instance.data["folderPath"] families = instance.data.get("families", []) + [ instance.data["family"] ] @@ -123,8 +115,8 @@ class ValidateEditorialAssetName(pyblish.api.ContextPlugin): parents = instance.data["parents"] - return_dict[asset] = [ + output[folder_path] = [ str(p["entity_name"]) for p in parents if p["entity_type"].lower() != "project" ] - return return_dict + return output diff --git a/client/ayon_core/plugins/publish/validate_unique_subsets.py b/client/ayon_core/plugins/publish/validate_unique_subsets.py index 3144675c50..4badeb8112 100644 --- a/client/ayon_core/plugins/publish/validate_unique_subsets.py +++ b/client/ayon_core/plugins/publish/validate_unique_subsets.py @@ -1,15 +1,17 @@ from collections import defaultdict + import pyblish.api + from ayon_core.pipeline.publish import ( PublishXmlValidationError, ) -class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): +class ValidateProductUniqueness(pyblish.api.ContextPlugin): """Validate all product names are unique. This only validates whether the instances currently set to publish from - the workfile overlap one another for the asset + product they are publishing + the workfile overlap one another for the folder + product they are publishing to. This does not perform any check against existing publishes in the database @@ -17,28 +19,28 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): versioning. A product may appear twice to publish from the workfile if one - of them is set to publish to another asset than the other. + of them is set to publish to another folder than the other. """ - label = "Validate Subset Uniqueness" + label = "Validate Product Uniqueness" order = pyblish.api.ValidatorOrder families = ["*"] def process(self, context): - # Find instance per (asset,product) - instance_per_asset_product = defaultdict(list) + # Find instance per (folder,product) + instance_per_folder_product = defaultdict(list) for instance in context: # Ignore disabled instances if not instance.data.get('publish', True): continue - # Ignore instance without asset data - asset = instance.data.get("folderPath") - if asset is None: - self.log.warning("Instance found without `asset` data: " + # Ignore instance without folder data + folder_path = instance.data.get("folderPath") + if folder_path is None: + self.log.warning("Instance found without `folderPath` data: " "{}".format(instance.name)) continue @@ -50,16 +52,21 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): ).format(instance.name)) continue - instance_per_asset_product[(asset, product_name)].append(instance) + instance_per_folder_product[(folder_path, product_name)].append( + instance + ) non_unique = [] - for (asset, product_name), instances in instance_per_asset_product.items(): - - # A single instance per asset, product is fine + for (folder_path, product_name), instances in ( + instance_per_folder_product.items() + ): + # A single instance per folder, product is fine if len(instances) < 2: continue - non_unique.append("{} > {}".format(asset, product_name)) + non_unique.append( + "{} > {}".format(folder_path, product_name) + ) if not non_unique: # All is ok diff --git a/client/ayon_core/settings/lib.py b/client/ayon_core/settings/lib.py index 69525d5b86..d72e4f357a 100644 --- a/client/ayon_core/settings/lib.py +++ b/client/ayon_core/settings/lib.py @@ -5,7 +5,7 @@ import collections import copy import time -from ayon_core.client import get_ayon_server_api_connection +import ayon_api log = logging.getLogger(__name__) @@ -46,8 +46,7 @@ class _AyonSettingsCache: @classmethod def _use_bundles(cls): if _AyonSettingsCache.use_bundles is None: - con = get_ayon_server_api_connection() - major, minor, _, _, _ = con.get_server_version_tuple() + major, minor, _, _, _ = ayon_api.get_server_version_tuple() use_bundles = True if (major, minor) < (0, 3): use_bundles = False @@ -69,8 +68,7 @@ class _AyonSettingsCache: _AyonSettingsCache.variant = variant # Set the variant to global ayon api connection - con = get_ayon_server_api_connection() - con.set_default_settings_variant(variant) + ayon_api.set_default_settings_variant(variant) return _AyonSettingsCache.variant @classmethod @@ -81,23 +79,21 @@ class _AyonSettingsCache: def get_value_by_project(cls, project_name): cache_item = _AyonSettingsCache.cache_by_project_name[project_name] if cache_item.is_outdated: - con = get_ayon_server_api_connection() if cls._use_bundles(): - value = con.get_addons_settings( + value = ayon_api.get_addons_settings( bundle_name=cls._get_bundle_name(), project_name=project_name, variant=cls._get_variant() ) else: - value = con.get_addons_settings(project_name) + value = ayon_api.get_addons_settings(project_name) cache_item.update_value(value) return cache_item.get_value() @classmethod def _get_addon_versions_from_bundle(cls): - con = get_ayon_server_api_connection() expected_bundle = cls._get_bundle_name() - bundles = con.get_bundles()["bundles"] + bundles = ayon_api.get_bundles()["bundles"] bundle = next( ( bundle @@ -117,8 +113,7 @@ class _AyonSettingsCache: if cls._use_bundles(): addons = cls._get_addon_versions_from_bundle() else: - con = get_ayon_server_api_connection() - settings_data = con.get_addons_settings( + settings_data = ayon_api.get_addons_settings( only_values=False, variant=cls._get_variant() ) diff --git a/client/ayon_core/tools/adobe_webserver/app.py b/client/ayon_core/tools/adobe_webserver/app.py index b10509f484..7d97d7d66d 100644 --- a/client/ayon_core/tools/adobe_webserver/app.py +++ b/client/ayon_core/tools/adobe_webserver/app.py @@ -81,15 +81,19 @@ class WebServerTool: await client.connect() context = get_global_context() - project = context["project_name"] - asset = context["folder_path"] - task = context["task_name"] - log.info("Sending context change to {}-{}-{}".format(project, - asset, - task)) + project_name = context["project_name"] + folder_path = context["folder_path"] + task_name = context["task_name"] + log.info("Sending context change to {}{}/{}".format( + project_name, folder_path, task_name + )) - await client.call('{}.set_context'.format(host), - project=project, asset=asset, task=task) + await client.call( + '{}.set_context'.format(host), + project=project_name, + folder=folder_path, + task=task_name + ) await client.close() def port_occupied(self, host_name, port): diff --git a/client/ayon_core/tools/assetlinks/__init__.py b/client/ayon_core/tools/assetlinks/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/client/ayon_core/tools/assetlinks/widgets.py b/client/ayon_core/tools/assetlinks/widgets.py deleted file mode 100644 index 7db6243358..0000000000 --- a/client/ayon_core/tools/assetlinks/widgets.py +++ /dev/null @@ -1,155 +0,0 @@ -import collections -from ayon_core.client import ( - get_versions, - get_subsets, - get_assets, - get_output_link_versions, -) - -from qtpy import QtWidgets - - -class SimpleLinkView(QtWidgets.QWidget): - def __init__(self, dbcon, parent): - super(SimpleLinkView, self).__init__(parent=parent) - self.dbcon = dbcon - - # TODO: display selected target - - in_text = QtWidgets.QLabel("Inputs") - in_view = QtWidgets.QListWidget(parent=self) - out_text = QtWidgets.QLabel("Outputs") - out_view = QtWidgets.QListWidget(parent=self) - - layout = QtWidgets.QGridLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(in_text, 0, 0) - layout.addWidget(in_view, 1, 0) - layout.addWidget(out_text, 0, 1) - layout.addWidget(out_view, 1, 1) - - self._in_view = in_view - self._out_view = out_view - self._version_doc_to_process = None - - @property - def project_name(self): - return self.dbcon.current_project() - - def clear(self): - self._in_view.clear() - self._out_view.clear() - - def set_version(self, version_doc): - self.clear() - self._version_doc_to_process = version_doc - if version_doc and self.isVisible(): - self._fill_values() - - def showEvent(self, event): - super(SimpleLinkView, self).showEvent(event) - self._fill_values() - - def _fill_values(self): - if self._version_doc_to_process is None: - return - version_doc = self._version_doc_to_process - self._version_doc_to_process = None - self._fill_inputs(version_doc) - self._fill_outputs(version_doc) - - def _fill_inputs(self, version_doc): - version_ids = set() - for link in version_doc["data"].get("inputLinks", []): - # Backwards compatibility for "input" key used as "id" - if "id" not in link: - link_id = link["input"] - else: - link_id = link["id"] - version_ids.add(link_id) - - version_docs = list(get_versions( - self.project_name, - version_ids=version_ids, - fields=["name", "parent"] - )) - - versions_by_subset_id = collections.defaultdict(list) - for version_doc in version_docs: - subset_id = version_doc["parent"] - versions_by_subset_id[subset_id].append(version_doc) - - subset_docs = [] - if versions_by_subset_id: - subset_docs = list(get_subsets( - self.project_name, - subset_ids=versions_by_subset_id.keys(), - fields=["_id", "name", "parent"] - )) - - asset_docs = [] - subsets_by_asset_id = collections.defaultdict(list) - if subset_docs: - for subset_doc in subset_docs: - asset_id = subset_doc["parent"] - subsets_by_asset_id[asset_id].append(subset_doc) - - asset_docs = list(get_assets( - self.project_name, - asset_ids=subsets_by_asset_id.keys(), - fields=["_id", "name"] - )) - - for asset_doc in asset_docs: - asset_id = asset_doc["_id"] - for subset_doc in subsets_by_asset_id[asset_id]: - subset_id = subset_doc["_id"] - for version_doc in versions_by_subset_id[subset_id]: - self._in_view.addItem("{} {} v{:0>3}".format( - asset_doc["name"], - subset_doc["name"], - version_doc["name"], - )) - - def _fill_outputs(self, version_doc): - version_docs = list(get_output_link_versions( - self.project_name, - version_doc["_id"], - fields=["name", "parent"] - )) - versions_by_subset_id = collections.defaultdict(list) - for version_doc in version_docs: - subset_id = version_doc["parent"] - versions_by_subset_id[subset_id].append(version_doc) - - subset_docs = [] - if versions_by_subset_id: - subset_docs = list(get_subsets( - self.project_name, - subset_ids=versions_by_subset_id.keys(), - fields=["_id", "name", "parent"] - )) - - asset_docs = [] - subsets_by_asset_id = collections.defaultdict(list) - if subset_docs: - for subset_doc in subset_docs: - asset_id = subset_doc["parent"] - subsets_by_asset_id[asset_id].append(subset_doc) - - asset_docs = list(get_assets( - self.project_name, - asset_ids=subsets_by_asset_id.keys(), - fields=["_id", "name"] - )) - - for asset_doc in asset_docs: - asset_id = asset_doc["_id"] - for subset_doc in subsets_by_asset_id[asset_id]: - subset_id = subset_doc["_id"] - for version_doc in versions_by_subset_id[subset_id]: - self._out_view.addItem("{} {} v{:0>3}".format( - asset_doc["name"], - subset_doc["name"], - version_doc["name"], - )) diff --git a/client/ayon_core/tools/ayon_utils/models/hierarchy.py b/client/ayon_core/tools/ayon_utils/models/hierarchy.py index 10495cf10b..d8b28f020d 100644 --- a/client/ayon_core/tools/ayon_utils/models/hierarchy.py +++ b/client/ayon_core/tools/ayon_utils/models/hierarchy.py @@ -380,6 +380,26 @@ class HierarchyModel(object): ) return items.get(folder_path) + def get_task_item_by_name( + self, project_name, folder_id, task_name, sender + ): + """Get task item by name and folder id. + + Args: + project_name (str): Project name. + folder_id (str): Folder id. + task_name (str): Task name. + sender (Union[str, None]): Who requested the task item. + + Returns: + Union[TaskItem, None]: Task item found by name and folder id. + + """ + for task_item in self.get_task_items(project_name, folder_id, sender): + if task_item.name == task_name: + return task_item + return None + def get_task_items(self, project_name, folder_id, sender): if not project_name or not folder_id: return [] diff --git a/client/ayon_core/tools/ayon_utils/models/thumbnails.py b/client/ayon_core/tools/ayon_utils/models/thumbnails.py index 86d6f3cba3..138cee4ea2 100644 --- a/client/ayon_core/tools/ayon_utils/models/thumbnails.py +++ b/client/ayon_core/tools/ayon_utils/models/thumbnails.py @@ -1,17 +1,234 @@ +import os +import time import collections import ayon_api - -from ayon_core.client.thumbnails import AYONThumbnailCache +import appdirs from .cache import NestedCacheItem +FileInfo = collections.namedtuple( + "FileInfo", + ("path", "size", "modification_time") +) + + +class ThumbnailsCache: + """Cache of thumbnails on local storage. + + Thumbnails are cached to appdirs to predefined directory. Each project has + own subfolder with thumbnails -> that's because each project has own + thumbnail id validation and file names are thumbnail ids with matching + extension. Extensions are predefined (.png and .jpeg). + + Cache has cleanup mechanism which is triggered on initialized by default. + + The cleanup has 2 levels: + 1. soft cleanup which remove all files that are older then 'days_alive' + 2. max size cleanup which remove all files until the thumbnails folder + contains less then 'max_filesize' + - this is time consuming so it's not triggered automatically + + Args: + cleanup (bool): Trigger soft cleanup (Cleanup expired thumbnails). + """ + + # Lifetime of thumbnails (in seconds) + # - default 3 days + days_alive = 3 + # Max size of thumbnail directory (in bytes) + # - default 2 Gb + max_filesize = 2 * 1024 * 1024 * 1024 + + def __init__(self, cleanup=True): + self._thumbnails_dir = None + self._days_alive_secs = self.days_alive * 24 * 60 * 60 + if cleanup: + self.cleanup() + + def get_thumbnails_dir(self): + """Root directory where thumbnails are stored. + + Returns: + str: Path to thumbnails root. + """ + + if self._thumbnails_dir is None: + # TODO use generic function + directory = appdirs.user_data_dir("AYON", "Ynput") + self._thumbnails_dir = os.path.join(directory, "thumbnails") + return self._thumbnails_dir + + thumbnails_dir = property(get_thumbnails_dir) + + def get_thumbnails_dir_file_info(self): + """Get information about all files in thumbnails directory. + + Returns: + List[FileInfo]: List of file information about all files. + """ + + thumbnails_dir = self.thumbnails_dir + files_info = [] + if not os.path.exists(thumbnails_dir): + return files_info + + for root, _, filenames in os.walk(thumbnails_dir): + for filename in filenames: + path = os.path.join(root, filename) + files_info.append(FileInfo( + path, os.path.getsize(path), os.path.getmtime(path) + )) + return files_info + + def get_thumbnails_dir_size(self, files_info=None): + """Got full size of thumbnail directory. + + Args: + files_info (List[FileInfo]): Prepared file information about + files in thumbnail directory. + + Returns: + int: File size of all files in thumbnail directory. + """ + + if files_info is None: + files_info = self.get_thumbnails_dir_file_info() + + if not files_info: + return 0 + + return sum( + file_info.size + for file_info in files_info + ) + + def cleanup(self, check_max_size=False): + """Cleanup thumbnails directory. + + Args: + check_max_size (bool): Also cleanup files to match max size of + thumbnails directory. + """ + + thumbnails_dir = self.get_thumbnails_dir() + # Skip if thumbnails dir does not exists yet + if not os.path.exists(thumbnails_dir): + return + + self._soft_cleanup(thumbnails_dir) + if check_max_size: + self._max_size_cleanup(thumbnails_dir) + + def _soft_cleanup(self, thumbnails_dir): + current_time = time.time() + for root, _, filenames in os.walk(thumbnails_dir): + for filename in filenames: + path = os.path.join(root, filename) + modification_time = os.path.getmtime(path) + if current_time - modification_time > self._days_alive_secs: + os.remove(path) + + def _max_size_cleanup(self, thumbnails_dir): + files_info = self.get_thumbnails_dir_file_info() + size = self.get_thumbnails_dir_size(files_info) + if size < self.max_filesize: + return + + sorted_file_info = collections.deque( + sorted(files_info, key=lambda item: item.modification_time) + ) + diff = size - self.max_filesize + while diff > 0: + if not sorted_file_info: + break + + file_info = sorted_file_info.popleft() + diff -= file_info.size + os.remove(file_info.path) + + def get_thumbnail_filepath(self, project_name, thumbnail_id): + """Get thumbnail by thumbnail id. + + Args: + project_name (str): Name of project. + thumbnail_id (str): Thumbnail id. + + Returns: + Union[str, None]: Path to thumbnail image or None if thumbnail + is not cached yet. + """ + + if not thumbnail_id: + return None + + for ext in ( + ".png", + ".jpeg", + ): + filepath = os.path.join( + self.thumbnails_dir, project_name, thumbnail_id + ext + ) + if os.path.exists(filepath): + return filepath + return None + + def get_project_dir(self, project_name): + """Path to root directory for specific project. + + Args: + project_name (str): Name of project for which root directory path + should be returned. + + Returns: + str: Path to root of project's thumbnails. + """ + + return os.path.join(self.thumbnails_dir, project_name) + + def make_sure_project_dir_exists(self, project_name): + project_dir = self.get_project_dir(project_name) + if not os.path.exists(project_dir): + os.makedirs(project_dir) + return project_dir + + def store_thumbnail(self, project_name, thumbnail_id, content, mime_type): + """Store thumbnail to cache folder. + + Args: + project_name (str): Project where the thumbnail belong to. + thumbnail_id (str): Id of thumbnail. + content (bytes): Byte content of thumbnail file. + mime_data (str): Type of content. + + Returns: + str: Path to cached thumbnail image file. + """ + + if mime_type == "image/png": + ext = ".png" + elif mime_type == "image/jpeg": + ext = ".jpeg" + else: + raise ValueError( + "Unknown mime type for thumbnail \"{}\"".format(mime_type)) + + project_dir = self.make_sure_project_dir_exists(project_name) + thumbnail_path = os.path.join(project_dir, thumbnail_id + ext) + with open(thumbnail_path, "wb") as stream: + stream.write(content) + + current_time = time.time() + os.utime(thumbnail_path, (current_time, current_time)) + + return thumbnail_path + class ThumbnailsModel: entity_cache_lifetime = 240 # In seconds def __init__(self): - self._thumbnail_cache = AYONThumbnailCache() + self._thumbnail_cache = ThumbnailsCache() self._paths_cache = collections.defaultdict(dict) self._folders_cache = NestedCacheItem( levels=2, lifetime=self.entity_cache_lifetime) diff --git a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py index b273d83fa6..cfe901c492 100644 --- a/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py +++ b/client/ayon_core/tools/ayon_utils/widgets/tasks_widget.py @@ -381,6 +381,15 @@ class TasksWidget(QtWidgets.QWidget): "task_type": task_type, } + def get_selected_task_id(self): + """Get selected task id. + + Returns: + Union[str, None]: Task id. + + """ + return self.get_selected_task_info()["task_id"] + def get_selected_task_name(self): """Get selected task name. @@ -388,8 +397,7 @@ class TasksWidget(QtWidgets.QWidget): Union[str, None]: Task name. """ - _, _, task_name, _ = self._get_selected_item_ids() - return task_name + return self.get_selected_task_info()["task_name"] def get_selected_task_type(self): """Get selected task type. @@ -398,8 +406,7 @@ class TasksWidget(QtWidgets.QWidget): Union[str, None]: Task type. """ - _, _, _, task_type = self._get_selected_item_ids() - return task_type + return self.get_selected_task_info()["task_type"] def set_selected_task(self, task_name): """Set selected task by name. diff --git a/client/ayon_core/tools/context_dialog/window.py b/client/ayon_core/tools/context_dialog/window.py index e2c9f71aaa..b145e77515 100644 --- a/client/ayon_core/tools/context_dialog/window.py +++ b/client/ayon_core/tools/context_dialog/window.py @@ -277,8 +277,8 @@ class ContextDialogController: def is_initial_context_valid(self): return self._initial_folder_found and self._initial_project_found - def set_initial_context(self, project_name=None, asset_name=None): - result = self._prepare_initial_context(project_name, asset_name) + def set_initial_context(self, project_name=None, folder_path=None): + result = self._prepare_initial_context(project_name, folder_path) self._initial_project_name = project_name self._initial_folder_id = result["folder_id"] @@ -352,7 +352,7 @@ class ContextDialogController: with open(self._output_path, "w") as stream: json.dump(self.get_selected_context(), stream, indent=4) - def _prepare_initial_context(self, project_name, asset_name): + def _prepare_initial_context(self, project_name, folder_path): project_found = True output = { "project_found": project_found, @@ -362,26 +362,26 @@ class ContextDialogController: "tasks_found": True, } if project_name is None: - asset_name = None + folder_path = None else: project = ayon_api.get_project(project_name) project_found = project is not None output["project_found"] = project_found - if not project_found or not asset_name: + if not project_found or not folder_path: return output - output["folder_label"] = asset_name + output["folder_label"] = folder_path folder_id = None folder_found = False # First try to find by path - folder = ayon_api.get_folder_by_path(project_name, asset_name) + folder = ayon_api.get_folder_by_path(project_name, folder_path) # Try to find by name if folder was not found by path - # - prevent to query by name if 'asset_name' contains '/' - if not folder and "/" not in asset_name: + # - prevent to query by name if 'folder_path' contains '/' + if not folder and "/" not in folder_path: folder = next( ayon_api.get_folders( - project_name, folder_names=[asset_name], fields=["id"]), + project_name, folder_names=[folder_path], fields=["id"]), None ) @@ -496,10 +496,10 @@ class ContextDialog(QtWidgets.QDialog): Context has 3 parts: - Project - - Asset + - Folder - Task - It is possible to predefine project and asset. In that case their widgets + It is possible to predefine project and folder. In that case their widgets will have passed preselected values and will be disabled. """ def __init__(self, controller=None, parent=None): @@ -521,7 +521,7 @@ class ContextDialog(QtWidgets.QDialog): # UI initialization main_splitter = QtWidgets.QSplitter(self) - # Left side widget contains project combobox and asset widget + # Left side widget contains project combobox and folders widget left_side_widget = QtWidgets.QWidget(main_splitter) project_combobox = ProjectsCombobox( @@ -531,7 +531,7 @@ class ContextDialog(QtWidgets.QDialog): ) project_combobox.set_select_item_visible(True) - # Assets widget + # Folders widget folders_widget = FoldersWidget( controller, parent=left_side_widget, @@ -661,11 +661,7 @@ class ContextDialog(QtWidgets.QDialog): self._controller.set_strict(enabled) def refresh(self): - """Refresh all widget one by one. - - When asset refresh is triggered we have to wait when is done so - this method continues with `_on_asset_widget_refresh_finished`. - """ + """Refresh all widget one by one.""" self._controller.reset() @@ -673,10 +669,10 @@ class ContextDialog(QtWidgets.QDialog): """Result of dialog.""" return self._controller.get_selected_context() - def set_context(self, project_name=None, asset_name=None): + def set_context(self, project_name=None, folder_path=None): """Set context which will be used and locked in dialog.""" - self._controller.set_initial_context(project_name, asset_name) + self._controller.set_initial_context(project_name, folder_path) def _on_projects_refresh(self): initial_context = self._controller.get_initial_context() @@ -784,14 +780,14 @@ class ContextDialog(QtWidgets.QDialog): def main( path_to_store, project_name=None, - asset_name=None, + folder_path=None, strict=True ): # Run Qt application app = get_ayon_qt_app() controller = ContextDialogController() controller.set_strict(strict) - controller.set_initial_context(project_name, asset_name) + controller.set_initial_context(project_name, folder_path) controller.set_output_json_path(path_to_store) window = ContextDialog(controller=controller) window.show() diff --git a/client/ayon_core/tools/creator/window.py b/client/ayon_core/tools/creator/window.py index 7bf65ea510..5bdc6da9b6 100644 --- a/client/ayon_core/tools/creator/window.py +++ b/client/ayon_core/tools/creator/window.py @@ -2,15 +2,15 @@ import sys import traceback import re +import ayon_api from qtpy import QtWidgets, QtCore -from ayon_core.client import get_asset_by_name, get_subsets from ayon_core import style from ayon_core.settings import get_current_project_settings from ayon_core.tools.utils.lib import qt_app_context from ayon_core.pipeline import ( get_current_project_name, - get_current_asset_name, + get_current_folder_path, get_current_task_name, ) from ayon_core.pipeline.create import ( @@ -216,20 +216,20 @@ class CreatorWindow(QtWidgets.QDialog): # Early exit if no folder path if not folder_path: self._build_menu() - self.echo("Asset name is required ..") + self.echo("Folder is required ..") self._set_valid_state(False) return project_name = get_current_project_name() - asset_doc = None + folder_entity = None if creator_plugin: - # Get the asset from the database which match with the name - asset_doc = get_asset_by_name( - project_name, folder_path, fields=["_id"] + # Get the folder from the database which match with the name + folder_entity = ayon_api.get_folder_by_path( + project_name, folder_path, fields={"id"} ) # Get plugin - if not asset_doc or not creator_plugin: + if not folder_entity or not creator_plugin: self._build_menu() if not creator_plugin: @@ -239,12 +239,16 @@ class CreatorWindow(QtWidgets.QDialog): self._set_valid_state(False) return - folder_id = asset_doc["_id"] + folder_id = folder_entity["id"] + task_name = get_current_task_name() + task_entity = ayon_api.get_task_by_name( + project_name, folder_id, task_name + ) # Calculate product name with Creator plugin product_name = creator_plugin.get_product_name( - project_name, folder_id, task_name, user_input_text + project_name, folder_entity, task_entity, user_input_text ) # Force replacement of prohibited symbols # QUESTION should Creator care about this and here should be only @@ -272,12 +276,12 @@ class CreatorWindow(QtWidgets.QDialog): self._product_name_input.setText(product_name) # Get all products of the current folder - subset_docs = get_subsets( - project_name, asset_ids=[folder_id], fields=["name"] + product_entities = ayon_api.get_products( + project_name, folder_ids={folder_id}, fields={"name"} ) existing_product_names = { - subset_doc["name"] - for subset_doc in subset_docs + product_entity["name"] + for product_entity in product_entities } existing_product_names_low = set( _name.lower() @@ -372,13 +376,13 @@ class CreatorWindow(QtWidgets.QDialog): self.setStyleSheet(style.load_stylesheet()) def refresh(self): - self._folder_path_input.setText(get_current_asset_name()) + self._folder_path_input.setText(get_current_folder_path()) self._creators_model.reset() product_types_smart_select = ( get_current_project_settings() - ["global"] + ["core"] ["tools"] ["creator"] ["product_types_smart_select"] diff --git a/client/ayon_core/tools/launcher/models/actions.py b/client/ayon_core/tools/launcher/models/actions.py index 6b9a33e57a..50c3e85b0a 100644 --- a/client/ayon_core/tools/launcher/models/actions.py +++ b/client/ayon_core/tools/launcher/models/actions.py @@ -309,9 +309,11 @@ class ActionsModel: task_name = None task_type = None if task_id is not None: - task = self._controller.get_task_entity(project_name, task_id) - task_name = task["name"] - task_type = task["taskType"] + task_entity = self._controller.get_task_entity( + project_name, task_id + ) + task_name = task_entity["name"] + task_type = task_entity["taskType"] output = should_start_last_workfile( project_name, diff --git a/client/ayon_core/tools/loader/control.py b/client/ayon_core/tools/loader/control.py index d2ee1d890c..5995bd2cae 100644 --- a/client/ayon_core/tools/loader/control.py +++ b/client/ayon_core/tools/loader/control.py @@ -320,10 +320,10 @@ class LoaderController(BackendLoaderController, FrontendLoaderController): context = get_current_context() folder_id = None project_name = context.get("project_name") - asset_name = context.get("folder_path") - if project_name and asset_name: + folder_path = context.get("folder_path") + if project_name and folder_path: folder = ayon_api.get_folder_by_path( - project_name, asset_name, fields=["id"] + project_name, folder_path, fields=["id"] ) if folder: folder_id = folder["id"] diff --git a/client/ayon_core/tools/loader/models/actions.py b/client/ayon_core/tools/loader/models/actions.py index dff15ea16c..aab5ba49d1 100644 --- a/client/ayon_core/tools/loader/models/actions.py +++ b/client/ayon_core/tools/loader/models/actions.py @@ -1,25 +1,19 @@ import sys import traceback import inspect -import copy import collections import uuid -from ayon_core.client import ( - get_project, - get_assets, - get_subsets, - get_versions, - get_representations, -) +import ayon_api + from ayon_core.pipeline.load import ( discover_loader_plugins, - SubsetLoaderPlugin, + ProductLoaderPlugin, filter_repre_contexts_by_loader, get_loader_identifier, load_with_repre_context, - load_with_subset_context, - load_with_subset_contexts, + load_with_product_context, + load_with_product_contexts, LoadError, IncompatibleLoaderError, ) @@ -33,14 +27,10 @@ NOT_SET = object() class LoaderActionsModel: """Model for loader actions. - This is probably only part of models that requires to use codebase from - 'ayon_core.client' because of backwards compatibility with loaders logic - which are expecting mongo documents. - TODOs: Deprecate 'qargparse' usage in loaders and implement conversion of 'ActionItem' to data (and 'from_data'). - Use controller to get entities (documents) -> possible only when + Use controller to get entities -> possible only when loaders are able to handle AYON vs. OpenPype logic. Add missing site sync logic, and if possible remove it from loaders. Implement loader actions to replace load plugins. @@ -317,7 +307,7 @@ class LoaderActionsModel: we want to show loaders for? Returns: - tuple[list[SubsetLoaderPlugin], list[LoaderPlugin]]: Discovered + tuple[list[ProductLoaderPlugin], list[LoaderPlugin]]: Discovered loader plugins. """ @@ -342,7 +332,7 @@ class LoaderActionsModel: identifier = get_loader_identifier(loader_cls) loaders_by_identifier[identifier] = loader_cls - if issubclass(loader_cls, SubsetLoaderPlugin): + if issubclass(loader_cls, ProductLoaderPlugin): product_loaders.append(loader_cls) else: repre_loaders.append(loader_cls) @@ -368,48 +358,15 @@ class LoaderActionsModel: return action_item.order, action_item.label - def _get_version_docs(self, project_name, version_ids): - """Get version documents for given version ids. - - This function also handles hero versions and copies data from - source version to it. - - Todos: - Remove this function when this is completely rewritten to - use AYON calls. - """ - - version_docs = list(get_versions( - project_name, version_ids=version_ids, hero=True - )) - hero_versions_by_src_id = collections.defaultdict(list) - src_hero_version = set() - for version_doc in version_docs: - if version_doc["type"] != "hero": - continue - version_id = "" - src_hero_version.add(version_id) - hero_versions_by_src_id[version_id].append(version_doc) - - src_versions = [] - if src_hero_version: - src_versions = get_versions(project_name, version_ids=version_ids) - for src_version in src_versions: - src_version_id = src_version["_id"] - for hero_version in hero_versions_by_src_id[src_version_id]: - hero_version["data"] = copy.deepcopy(src_version["data"]) - - return version_docs - def _contexts_for_versions(self, project_name, version_ids): """Get contexts for given version ids. - Prepare version contexts for 'SubsetLoaderPlugin' and representation + Prepare version contexts for 'ProductLoaderPlugin' and representation contexts for 'LoaderPlugin' for all children representations of given versions. This method is very similar to '_contexts_for_representations' but the - queries of documents are called in a different order. + queries of entities are called in a different order. Args: project_name (str): Project name. @@ -426,55 +383,59 @@ class LoaderActionsModel: if not project_name and not version_ids: return version_context_by_id, repre_context_by_id - version_docs = self._get_version_docs(project_name, version_ids) - version_docs_by_id = {} - version_docs_by_product_id = collections.defaultdict(list) - for version_doc in version_docs: - version_id = version_doc["_id"] - product_id = version_doc["parent"] - version_docs_by_id[version_id] = version_doc - version_docs_by_product_id[product_id].append(version_doc) + version_entities = ayon_api.get_versions( + project_name, version_ids=version_ids + ) + version_entities_by_id = {} + version_entities_by_product_id = collections.defaultdict(list) + for version_entity in version_entities: + version_id = version_entity["id"] + product_id = version_entity["productId"] + version_entities_by_id[version_id] = version_entity + version_entities_by_product_id[product_id].append(version_entity) - _product_ids = set(version_docs_by_product_id.keys()) - _product_docs = get_subsets(project_name, subset_ids=_product_ids) - product_docs_by_id = {p["_id"]: p for p in _product_docs} + _product_ids = set(version_entities_by_product_id.keys()) + _product_entities = ayon_api.get_products( + project_name, product_ids=_product_ids + ) + product_entities_by_id = {p["id"]: p for p in _product_entities} - _folder_ids = {p["parent"] for p in product_docs_by_id.values()} - _folder_docs = get_assets(project_name, asset_ids=_folder_ids) - folder_docs_by_id = {f["_id"]: f for f in _folder_docs} + _folder_ids = {p["folderId"] for p in product_entities_by_id.values()} + _folder_entities = ayon_api.get_folders( + project_name, folder_ids=_folder_ids + ) + folder_entities_by_id = {f["id"]: f for f in _folder_entities} - project_doc = get_project(project_name) - project_doc["code"] = project_doc["data"]["code"] + project_entity = ayon_api.get_project(project_name) - for version_doc in version_docs: - version_id = version_doc["_id"] - product_id = version_doc["parent"] - product_doc = product_docs_by_id[product_id] - folder_id = product_doc["parent"] - folder_doc = folder_docs_by_id[folder_id] + for version_id, version_entity in version_entities_by_id.items(): + product_id = version_entity["productId"] + product_entity = product_entities_by_id[product_id] + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] version_context_by_id[version_id] = { - "project": project_doc, - "asset": folder_doc, - "subset": product_doc, - "version": version_doc, + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, } - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, version_ids=version_ids) - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_doc = version_docs_by_id[version_id] - product_id = version_doc["parent"] - product_doc = product_docs_by_id[product_id] - folder_id = product_doc["parent"] - folder_doc = folder_docs_by_id[folder_id] + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + version_entity = version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = product_entities_by_id[product_id] + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] - repre_context_by_id[repre_doc["_id"]] = { - "project": project_doc, - "asset": folder_doc, - "subset": product_doc, - "version": version_doc, - "representation": repre_doc, + repre_context_by_id[repre_entity["id"]] = { + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, + "representation": repre_entity, } return version_context_by_id, repre_context_by_id @@ -482,12 +443,12 @@ class LoaderActionsModel: def _contexts_for_representations(self, project_name, repre_ids): """Get contexts for given representation ids. - Prepare version contexts for 'SubsetLoaderPlugin' and representation + Prepare version contexts for 'ProductLoaderPlugin' and representation contexts for 'LoaderPlugin' for all children representations of given versions. This method is very similar to '_contexts_for_versions' but the - queries of documents are called in a different order. + queries of entities are called in a different order. Args: project_name (str): Project name. @@ -503,53 +464,58 @@ class LoaderActionsModel: if not project_name and not repre_ids: return product_context_by_id, repre_context_by_id - repre_docs = list(get_representations( + repre_entities = list(ayon_api.get_representations( project_name, representation_ids=repre_ids )) - version_ids = {r["parent"] for r in repre_docs} - version_docs = self._get_version_docs(project_name, version_ids) - version_docs_by_id = { - v["_id"]: v for v in version_docs + version_ids = {r["versionId"] for r in repre_entities} + version_entities = ayon_api.get_versions( + project_name, version_ids=version_ids + ) + version_entities_by_id = { + v["id"]: v for v in version_entities } - product_ids = {v["parent"] for v in version_docs_by_id.values()} - product_docs = get_subsets(project_name, subset_ids=product_ids) - product_docs_by_id = { - p["_id"]: p for p in product_docs + product_ids = {v["productId"] for v in version_entities_by_id.values()} + product_entities = ayon_api.get_products( + project_name, product_ids=product_ids + ) + product_entities_by_id = { + p["id"]: p for p in product_entities } - folder_ids = {p["parent"] for p in product_docs_by_id.values()} - folder_docs = get_assets(project_name, asset_ids=folder_ids) - folder_docs_by_id = { - f["_id"]: f for f in folder_docs + folder_ids = {p["folderId"] for p in product_entities_by_id.values()} + folder_entities = ayon_api.get_folders( + project_name, folder_ids=folder_ids + ) + folder_entities_by_id = { + f["id"]: f for f in folder_entities } - project_doc = get_project(project_name) - project_doc["code"] = project_doc["data"]["code"] + project_entity = ayon_api.get_project(project_name) - for product_id, product_doc in product_docs_by_id.items(): - folder_id = product_doc["parent"] - folder_doc = folder_docs_by_id[folder_id] + for product_id, product_entity in product_entities_by_id.items(): + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] product_context_by_id[product_id] = { - "project": project_doc, - "asset": folder_doc, - "subset": product_doc, + "project": project_entity, + "folder": folder_entity, + "product": product_entity, } - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_doc = version_docs_by_id[version_id] - product_id = version_doc["parent"] - product_doc = product_docs_by_id[product_id] - folder_id = product_doc["parent"] - folder_doc = folder_docs_by_id[folder_id] + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + version_entity = version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = product_entities_by_id[product_id] + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] - repre_context_by_id[repre_doc["_id"]] = { - "project": project_doc, - "asset": folder_doc, - "subset": product_doc, - "version": version_doc, - "representation": repre_doc, + repre_context_by_id[repre_entity["id"]] = { + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, + "representation": repre_entity, } return product_context_by_id, repre_context_by_id @@ -594,10 +560,10 @@ class LoaderActionsModel: repre_product_ids = set() repre_folder_ids = set() for repre_context in filtered_repre_contexts: - repre_ids.add(repre_context["representation"]["_id"]) - repre_product_ids.add(repre_context["subset"]["_id"]) - repre_version_ids.add(repre_context["version"]["_id"]) - repre_folder_ids.add(repre_context["asset"]["_id"]) + repre_ids.add(repre_context["representation"]["id"]) + repre_product_ids.add(repre_context["product"]["id"]) + repre_version_ids.add(repre_context["version"]["id"]) + repre_folder_ids.add(repre_context["folder"]["id"]) item = self._create_loader_action_item( loader, @@ -611,13 +577,13 @@ class LoaderActionsModel: ) action_items.append(item) - # Subset Loaders. + # Product Loaders. version_ids = set(version_context_by_id.keys()) product_folder_ids = set() product_ids = set() for product_context in version_context_by_id.values(): - product_ids.add(product_context["subset"]["_id"]) - product_folder_ids.add(product_context["asset"]["_id"]) + product_ids.add(product_context["product"]["id"]) + product_folder_ids.add(product_context["folder"]["id"]) version_contexts = list(version_context_by_id.values()) for loader in product_loaders: @@ -643,44 +609,49 @@ class LoaderActionsModel: ): """Trigger version loader. - This triggers 'load' method of 'SubsetLoaderPlugin' for given version + This triggers 'load' method of 'ProductLoaderPlugin' for given version ids. Note: - Even when the plugin is 'SubsetLoaderPlugin' it actually expects + Even when the plugin is 'ProductLoaderPlugin' it actually expects versions and should be named 'VersionLoaderPlugin'. Because it is planned to refactor load system and introduce 'LoaderAction' plugins it is not relevant to change it anymore. Args: - loader (SubsetLoaderPlugin): Loader plugin to use. + loader (ProductLoaderPlugin): Loader plugin to use. options (dict): Option values for loader. project_name (str): Project name. version_ids (Iterable[str]): Version ids. """ - project_doc = get_project(project_name) - project_doc["code"] = project_doc["data"]["code"] + project_entity = ayon_api.get_project(project_name) - version_docs = self._get_version_docs(project_name, version_ids) - product_ids = {v["parent"] for v in version_docs} - product_docs = get_subsets(project_name, subset_ids=product_ids) - product_docs_by_id = {f["_id"]: f for f in product_docs} - folder_ids = {p["parent"] for p in product_docs_by_id.values()} - folder_docs = get_assets(project_name, asset_ids=folder_ids) - folder_docs_by_id = {f["_id"]: f for f in folder_docs} + version_entities = list(ayon_api.get_versions( + project_name, version_ids=version_ids + )) + product_ids = {v["productId"] for v in version_entities} + product_entities = ayon_api.get_products( + project_name, product_ids=product_ids + ) + product_entities_by_id = {p["id"]: p for p in product_entities} + folder_ids = {p["folderId"] for p in product_entities_by_id.values()} + folder_entities = ayon_api.get_folders( + project_name, folder_ids=folder_ids + ) + folder_entities_by_id = {f["id"]: f for f in folder_entities} product_contexts = [] - for version_doc in version_docs: - product_id = version_doc["parent"] - product_doc = product_docs_by_id[product_id] - folder_id = product_doc["parent"] - folder_doc = folder_docs_by_id[folder_id] + for version_entity in version_entities: + product_id = version_entity["productId"] + product_entity = product_entities_by_id[product_id] + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] product_contexts.append({ - "project": project_doc, - "asset": folder_doc, - "subset": product_doc, - "version": version_doc, + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, }) return self._load_products_by_loader( @@ -698,7 +669,7 @@ class LoaderActionsModel: This triggers 'load' method of 'LoaderPlugin' for given representation ids. For that are prepared contexts for each representation, with - all parent documents. + all parent entities. Args: loader (LoaderPlugin): Loader plugin to use. @@ -707,34 +678,41 @@ class LoaderActionsModel: representation_ids (Iterable[str]): Representation ids. """ - project_doc = get_project(project_name) - project_doc["code"] = project_doc["data"]["code"] - repre_docs = list(get_representations( + project_entity = ayon_api.get_project(project_name) + repre_entities = list(ayon_api.get_representations( project_name, representation_ids=representation_ids )) - version_ids = {r["parent"] for r in repre_docs} - version_docs = self._get_version_docs(project_name, version_ids) - version_docs_by_id = {v["_id"]: v for v in version_docs} - product_ids = {v["parent"] for v in version_docs_by_id.values()} - product_docs = get_subsets(project_name, subset_ids=product_ids) - product_docs_by_id = {p["_id"]: p for p in product_docs} - folder_ids = {p["parent"] for p in product_docs_by_id.values()} - folder_docs = get_assets(project_name, asset_ids=folder_ids) - folder_docs_by_id = {f["_id"]: f for f in folder_docs} + version_ids = {r["versionId"] for r in repre_entities} + version_entities = ayon_api.get_versions( + project_name, version_ids=version_ids + ) + version_entities_by_id = {v["id"]: v for v in version_entities} + product_ids = { + v["productId"] for v in version_entities_by_id.values() + } + product_entities = ayon_api.get_products( + project_name, product_ids=product_ids + ) + product_entities_by_id = {p["id"]: p for p in product_entities} + folder_ids = {p["folderId"] for p in product_entities_by_id.values()} + folder_entities = ayon_api.get_folders( + project_name, folder_ids=folder_ids + ) + folder_entities_by_id = {f["id"]: f for f in folder_entities} repre_contexts = [] - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - version_doc = version_docs_by_id[version_id] - product_id = version_doc["parent"] - product_doc = product_docs_by_id[product_id] - folder_id = product_doc["parent"] - folder_doc = folder_docs_by_id[folder_id] + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + version_entity = version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = product_entities_by_id[product_id] + folder_id = product_entity["folderId"] + folder_entity = folder_entities_by_id[folder_id] repre_contexts.append({ - "project": project_doc, - "asset": folder_doc, - "subset": product_doc, - "version": version_doc, - "representation": repre_doc, + "project": project_entity, + "folder": folder_entity, + "product": product_entity, + "version": version_entity, + "representation": repre_entity, }) return self._load_representations_by_loader( @@ -747,18 +725,17 @@ class LoaderActionsModel: Args: loader (LoaderPlugin): Loader plugin to use. repre_contexts (list[dict]): Full info about selected - representations, containing repre, version, subset, asset and - project documents. + representations, containing repre, version, product, folder + and project entities. options (dict): Data from options. """ error_info = [] for repre_context in repre_contexts: - version_doc = repre_context["version"] - if version_doc["type"] == "hero_version": - version_name = "Hero" - else: - version_name = version_doc.get("name") + version_entity = repre_context["version"] + version = version_entity["version"] + if version < 0: + version = "Hero" try: load_with_repre_context( loader, @@ -772,8 +749,8 @@ class LoaderActionsModel: "Incompatible Loader", None, repre_context["representation"]["name"], - repre_context["subset"]["name"], - version_name + repre_context["product"]["name"], + version )) except Exception as exc: @@ -788,20 +765,20 @@ class LoaderActionsModel: str(exc), formatted_traceback, repre_context["representation"]["name"], - repre_context["subset"]["name"], - version_name + repre_context["product"]["name"], + version )) return error_info def _load_products_by_loader(self, loader, version_contexts, options): - """Triggers load with SubsetLoader type of loaders. + """Triggers load with ProductLoader type of loaders. Warning: - Plugin is named 'SubsetLoader' but version is passed to context + Plugin is named 'ProductLoader' but version is passed to context too. Args: - loader (SubsetLoder): Loader used to load. + loader (ProductLoader): Loader used to load. version_contexts (list[dict[str, Any]]): For context for each version. options (dict[str, Any]): Options for loader that user could fill. @@ -811,10 +788,10 @@ class LoaderActionsModel: if loader.is_multiple_contexts_compatible: product_names = [] for context in version_contexts: - product_name = context.get("subset", {}).get("name") or "N/A" + product_name = context.get("product", {}).get("name") or "N/A" product_names.append(product_name) try: - load_with_subset_contexts( + load_with_product_contexts( loader, version_contexts, options=options @@ -837,10 +814,10 @@ class LoaderActionsModel: else: for version_context in version_contexts: product_name = ( - version_context.get("subset", {}).get("name") or "N/A" + version_context.get("product", {}).get("name") or "N/A" ) try: - load_with_subset_context( + load_with_product_context( loader, version_context, options=options diff --git a/client/ayon_core/tools/loader/models/site_sync.py b/client/ayon_core/tools/loader/models/site_sync.py index 2a6f1558ad..daa9f7ba50 100644 --- a/client/ayon_core/tools/loader/models/site_sync.py +++ b/client/ayon_core/tools/loader/models/site_sync.py @@ -1,8 +1,8 @@ import collections +from ayon_api import get_representations, get_versions_links + from ayon_core.lib import Logger -from ayon_core.client.entities import get_representations -from ayon_core.client import get_linked_representation_id from ayon_core.addon import AddonsManager from ayon_core.tools.ayon_utils.models import NestedCacheItem from ayon_core.tools.loader.abstract import ActionItem @@ -322,24 +322,34 @@ class SiteSyncModel: active_site = self.get_active_site(project_name) remote_site = self.get_remote_site(project_name) - repre_docs = list(get_representations( - project_name, representation_ids=representation_ids - )) - product_type_by_repre_id = { - item["_id"]: item["context"]["family"] - for item in repre_docs + repre_entities_by_id = { + repre_entity["id"]: repre_entity + for repre_entity in get_representations( + project_name, representation_ids=representation_ids + ) } + # TODO get product type from product entity instead of 'context' + # on representation + product_type_by_repre_id = {} + for repre_id, repre_entity in repre_entities_by_id.items(): + repre_context = repre_entity["context"] + product_type = repre_context.get("product", {}).get("type") + if not product_type: + product_type = repre_context.get("family") + + product_type_by_repre_id[repre_id] = product_type for repre_id in representation_ids: + repre_entity = repre_entities_by_id.get(repre_id) product_type = product_type_by_repre_id[repre_id] if identifier == DOWNLOAD_IDENTIFIER: self._add_site( - project_name, repre_id, active_site, product_type + project_name, repre_entity, active_site, product_type ) elif identifier == UPLOAD_IDENTIFIER: self._add_site( - project_name, repre_id, remote_site, product_type + project_name, repre_entity, remote_site, product_type ) elif identifier == REMOVE_IDENTIFIER: @@ -485,19 +495,19 @@ class SiteSyncModel: representation_ids=representation_ids, ) - def _add_site(self, project_name, repre_id, site_name, product_type): + def _add_site(self, project_name, repre_entity, site_name, product_type): self._site_sync_addon.add_site( - project_name, repre_id, site_name, force=True + project_name, repre_entity["id"], site_name, force=True ) # TODO this should happen in site sync addon if product_type != "workfile": return - links = get_linked_representation_id( + links = self._get_linked_representation_id( project_name, - repre_id=repre_id, - link_type="reference" + repre_entity, + "reference" ) for link_repre_id in links: try: @@ -512,3 +522,83 @@ class SiteSyncModel: except Exception: # do not add/reset working site for references log.debug("Site present", exc_info=True) + + def _get_linked_representation_id( + self, + project_name, + repre_entity, + link_type, + max_depth=None + ): + """Returns list of linked ids of particular type (if provided). + + One of representation document or representation id must be passed. + Note: + Representation links now works only from representation through + version back to representations. + + Todos: + Missing depth query. Not sure how it did find more representations + in depth, probably links to version? + This function should probably live in sitesync addon? + + Args: + project_name (str): Name of project where look for links. + repre_entity (dict[str, Any]): Representation entity. + link_type (str): Type of link (e.g. 'reference', ...). + max_depth (int): Limit recursion level. Default: 0 + + Returns: + List[ObjectId] Linked representation ids. + """ + + if not repre_entity: + return [] + + version_id = repre_entity["versionId"] + if max_depth is None or max_depth == 0: + max_depth = 1 + + link_types = None + if link_type: + link_types = [link_type] + + # Store already found version ids to avoid recursion, and also to store + # output -> Don't forget to remove 'version_id' at the end!!! + linked_version_ids = {version_id} + # Each loop of depth will reset this variable + versions_to_check = {version_id} + for _ in range(max_depth): + if not versions_to_check: + break + + versions_links = get_versions_links( + project_name, + versions_to_check, + link_types=link_types, + link_direction="out") + + versions_to_check = set() + for links in versions_links.values(): + for link in links: + # Care only about version links + if link["entityType"] != "version": + continue + entity_id = link["entityId"] + # Skip already found linked version ids + if entity_id in linked_version_ids: + continue + linked_version_ids.add(entity_id) + versions_to_check.add(entity_id) + + linked_version_ids.remove(version_id) + if not linked_version_ids: + return [] + representations = get_representations( + project_name, + version_ids=linked_version_ids, + fields=["id"]) + return [ + repre["id"] + for repre in representations + ] diff --git a/client/ayon_core/tools/loader/ui/products_delegates.py b/client/ayon_core/tools/loader/ui/products_delegates.py index 53d35c2bb7..12ed1165ae 100644 --- a/client/ayon_core/tools/loader/ui/products_delegates.py +++ b/client/ayon_core/tools/loader/ui/products_delegates.py @@ -50,9 +50,7 @@ class VersionComboBox(QtWidgets.QComboBox): item = self._items_by_id.get(version_id) if item is None: - label = format_version( - abs(version_item.version), version_item.is_hero - ) + label = format_version(version_item.version) item = QtGui.QStandardItem(label) item.setData(version_id, QtCore.Qt.UserRole) self._items_by_id[version_id] = item @@ -85,7 +83,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): def displayText(self, value, locale): if not isinstance(value, numbers.Integral): return "N/A" - return format_version(abs(value), value < 0) + return format_version(value) def paint(self, painter, option, index): fg_color = index.data(QtCore.Qt.ForegroundRole) diff --git a/client/ayon_core/tools/publisher/control.py b/client/ayon_core/tools/publisher/control.py index 712142f662..aaca0fea10 100644 --- a/client/ayon_core/tools/publisher/control.py +++ b/client/ayon_core/tools/publisher/control.py @@ -14,10 +14,6 @@ import arrow import pyblish.api import ayon_api -from ayon_core.client import ( - get_asset_by_name, - get_subsets, -) from ayon_core.lib.events import QueuedEventSystem from ayon_core.lib.attribute_definitions import ( UIDef, @@ -66,24 +62,6 @@ class MainThreadItem: self.callback(*self.args, **self.kwargs) -class AssetDocsCache: - """Cache asset documents for creation part.""" - - def __init__(self, controller): - self._controller = controller - self._asset_docs_by_path = {} - - def reset(self): - self._asset_docs_by_path = {} - - def get_asset_doc_by_folder_path(self, folder_path): - if folder_path not in self._asset_docs_by_path: - project_name = self._controller.project_name - asset_doc = get_asset_by_name(project_name, folder_path) - self._asset_docs_by_path[folder_path] = asset_doc - return copy.deepcopy(self._asset_docs_by_path[folder_path]) - - class PublishReportMaker: """Report for single publishing process. @@ -1653,7 +1631,6 @@ class PublisherController(BasePublisherController): # Cacher of avalon documents self._hierarchy_model = HierarchyModel(self) - self._asset_docs_cache = AssetDocsCache(self) @property def project_name(self): @@ -1673,7 +1650,7 @@ class PublisherController(BasePublisherController): Union[str, None]: Folder path or None if folder is not set. """ - return self._create_context.get_current_asset_name() + return self._create_context.get_current_folder_path() @property def current_task_name(self): @@ -1796,14 +1773,14 @@ class PublisherController(BasePublisherController): if not folder_item: return None - subset_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=[folder_item.entity_id], - fields=["name"] + folder_ids={folder_item.entity_id}, + fields={"name"} ) return { - subset_doc["name"] - for subset_doc in subset_docs + product_entity["name"] + for product_entity in product_entities } def reset(self): @@ -1816,11 +1793,10 @@ class PublisherController(BasePublisherController): self._create_context.reset_preparation() - # Reset avalon context + # Reset current context self._create_context.reset_current_context() self._hierarchy_model.reset() - self._asset_docs_cache.reset() self._reset_plugins() # Publish part must be reset after plugins @@ -2052,16 +2028,37 @@ class PublisherController(BasePublisherController): """ creator = self._creators[creator_identifier] - project_name = self.project_name - asset_doc = self._asset_docs_cache.get_asset_doc_by_folder_path( - folder_path - ) + instance = None if instance_id: instance = self.instances[instance_id] + project_name = self.project_name + folder_item = self._hierarchy_model.get_folder_item_by_path( + project_name, folder_path + ) + folder_entity = None + task_item = None + task_entity = None + if folder_item is not None: + folder_entity = self._hierarchy_model.get_folder_entity( + project_name, folder_item.entity_id + ) + task_item = self._hierarchy_model.get_task_item_by_name( + project_name, folder_item.entity_id, task_name, "controller" + ) + + if task_item is not None: + task_entity = self._hierarchy_model.get_task_entity( + project_name, task_item.task_id + ) + return creator.get_product_name( - project_name, asset_doc, task_name, variant, instance=instance + project_name, + folder_entity, + task_entity, + variant, + instance=instance ) def trigger_convertor_items(self, convertor_identifiers): diff --git a/client/ayon_core/tools/publisher/widgets/widgets.py b/client/ayon_core/tools/publisher/widgets/widgets.py index 4005cf2c84..12c03c7eeb 100644 --- a/client/ayon_core/tools/publisher/widgets/widgets.py +++ b/client/ayon_core/tools/publisher/widgets/widgets.py @@ -1206,7 +1206,7 @@ class GlobalAttrsWidget(QtWidgets.QWidget): if folder_path is not None: instance["folderPath"] = folder_path - instance.set_asset_invalid(False) + instance.set_folder_invalid(False) if task_name is not None: instance["task"] = task_name or None diff --git a/client/ayon_core/tools/push_to_project/control.py b/client/ayon_core/tools/push_to_project/control.py index 3b6bd85028..d5acaadc2a 100644 --- a/client/ayon_core/tools/push_to_project/control.py +++ b/client/ayon_core/tools/push_to_project/control.py @@ -1,11 +1,7 @@ import threading -from ayon_core.client import ( - get_asset_by_id, - get_subset_by_id, - get_version_by_id, - get_representations, -) +import ayon_api + from ayon_core.settings import get_project_settings from ayon_core.lib import prepare_template_data from ayon_core.lib.events import QueuedEventSystem @@ -32,9 +28,10 @@ class PushToContextController: self._src_project_name = None self._src_version_id = None - self._src_asset_doc = None - self._src_subset_doc = None - self._src_version_doc = None + self._src_folder_entity = None + self._src_folder_task_entities = {} + self._src_product_entity = None + self._src_version_entity = None self._src_label = None self._submission_enabled = False @@ -71,28 +68,44 @@ class PushToContextController: self._src_project_name = project_name self._src_version_id = version_id self._src_label = None - asset_doc = None - subset_doc = None - version_doc = None + folder_entity = None + task_entities = {} + product_entity = None + version_entity = None if project_name and version_id: - version_doc = get_version_by_id(project_name, version_id) + version_entity = ayon_api.get_version_by_id( + project_name, version_id + ) - if version_doc: - subset_doc = get_subset_by_id(project_name, version_doc["parent"]) + if version_entity: + product_entity = ayon_api.get_product_by_id( + project_name, version_entity["productId"] + ) - if subset_doc: - asset_doc = get_asset_by_id(project_name, subset_doc["parent"]) + if product_entity: + folder_entity = ayon_api.get_folder_by_id( + project_name, product_entity["folderId"] + ) - self._src_asset_doc = asset_doc - self._src_subset_doc = subset_doc - self._src_version_doc = version_doc - if asset_doc: - self._user_values.set_new_folder_name(asset_doc["name"]) + if folder_entity: + task_entities = { + task_entity["name"]: task_entity + for task_entity in ayon_api.get_tasks( + project_name, folder_ids=[folder_entity["id"]] + ) + } + + self._src_folder_entity = folder_entity + self._src_folder_task_entities = task_entities + self._src_product_entity = product_entity + self._src_version_entity = version_entity + if folder_entity: + self._user_values.set_new_folder_name(folder_entity["name"]) variant = self._get_src_variant() if variant: self._user_values.set_variant(variant) - comment = version_doc["data"].get("comment") + comment = version_entity["attrib"].get("comment") if comment: self._user_values.set_comment(comment) @@ -197,39 +210,37 @@ class PushToContextController: if not self._src_project_name or not self._src_version_id: return "Source is not defined" - asset_doc = self._src_asset_doc - if not asset_doc: + folder_entity = self._src_folder_entity + if not folder_entity: return "Source is invalid" - folder_path_parts = list(asset_doc["data"]["parents"]) - folder_path_parts.append(asset_doc["name"]) - folder_path = "/".join(folder_path_parts) - subset_doc = self._src_subset_doc - version_doc = self._src_version_doc - return "Source: {}/{}/{}/v{:0>3}".format( + folder_path = folder_entity["path"] + product_entity = self._src_product_entity + version_entity = self._src_version_entity + return "Source: {}{}/{}/v{:0>3}".format( self._src_project_name, folder_path, - subset_doc["name"], - version_doc["name"] + product_entity["name"], + version_entity["version"] ) - def _get_task_info_from_repre_docs(self, asset_doc, repre_docs): - asset_tasks = asset_doc["data"].get("tasks") or {} + def _get_task_info_from_repre_entities( + self, task_entities, repre_entities + ): found_comb = [] - for repre_doc in repre_docs: - context = repre_doc["context"] - task_info = context.get("task") - if task_info is None: + for repre_entity in repre_entities: + context = repre_entity["context"] + repre_task_name = context.get("task") + if repre_task_name is None: continue + if isinstance(repre_task_name, dict): + repre_task_name = repre_task_name.get("name") + task_name = None task_type = None - if isinstance(task_info, str): - task_name = task_info - asset_task_info = asset_tasks.get(task_info) or {} - task_type = asset_task_info.get("type") - - elif isinstance(task_info, dict): + if repre_task_name: + task_info = task_entities.get(repre_task_name) or {} task_name = task_info.get("name") task_type = task_info.get("type") @@ -245,20 +256,17 @@ class PushToContextController: def _get_src_variant(self): project_name = self._src_project_name - version_doc = self._src_version_doc - asset_doc = self._src_asset_doc - repre_docs = get_representations( - project_name, version_ids=[version_doc["_id"]] + version_entity = self._src_version_entity + task_entities = self._src_folder_task_entities + repre_entities = ayon_api.get_representations( + project_name, version_ids={version_entity["id"]} ) - task_name, task_type = self._get_task_info_from_repre_docs( - asset_doc, repre_docs + task_name, task_type = self._get_task_info_from_repre_entities( + task_entities, repre_entities ) project_settings = get_project_settings(project_name) - subset_doc = self._src_subset_doc - product_type = subset_doc["data"].get("family") - if not product_type: - product_type = subset_doc["data"]["families"][0] + product_type = self._src_product_entity["productType"] template = get_product_name_template( self._src_project_name, product_type, @@ -292,7 +300,7 @@ class PushToContextController: print("Failed format", exc) return "" - product_name = self._src_subset_doc["name"] + product_name = self._src_product_entity["name"] if ( (product_s and not product_name.startswith(product_s)) or (product_e and not product_name.endswith(product_e)) 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 b427f3d226..8a29da2fe4 100644 --- a/client/ayon_core/tools/push_to_project/models/integrate.py +++ b/client/ayon_core/tools/push_to_project/models/integrate.py @@ -1,41 +1,25 @@ import os import re import copy -import socket import itertools -import datetime import sys import traceback import uuid -from ayon_core.client import ( - get_project, - get_assets, - get_asset_by_id, - get_subset_by_id, - get_subset_by_name, - get_version_by_id, - get_last_version_by_subset_id, - get_version_by_name, - get_representations, -) -from ayon_core.client.operations import ( +import ayon_api +from ayon_api.utils import create_entity_id +from ayon_api.operations import ( OperationsSession, - new_asset_document, - new_subset_document, - new_version_doc, - new_representation_doc, - prepare_version_update_data, - prepare_representation_update_data, -) -from ayon_core.addon import AddonsManager -from ayon_core.lib import ( - StringTemplate, - get_ayon_username, - get_formatted_current_time, - source_hash, + new_folder_entity, + new_product_entity, + new_version_entity, + new_representation_entity, ) +from ayon_core.lib import ( + StringTemplate, + source_hash, +) from ayon_core.lib.file_transaction import FileTransaction from ayon_core.settings import get_project_settings from ayon_core.pipeline import Anatomy @@ -235,20 +219,20 @@ class ProjectPushRepreItem: but filenames are not template based. Args: - repre_doc (Dict[str, Ant]): Representation document. + repre_entity (Dict[str, Ant]): Representation entity. roots (Dict[str, str]): Project roots (based on project anatomy). """ - def __init__(self, repre_doc, roots): - self._repre_doc = repre_doc + def __init__(self, repre_entity, roots): + self._repre_entity = repre_entity self._roots = roots self._src_files = None self._resource_files = None self._frame = UNKNOWN @property - def repre_doc(self): - return self._repre_doc + def repre_entity(self): + return self._repre_entity @property def src_files(self): @@ -327,7 +311,7 @@ class ProjectPushRepreItem: if self._src_files is not None: return self._src_files, self._resource_files - repre_context = self._repre_doc["context"] + repre_context = self._repre_entity["context"] if "frame" in repre_context or "udim" in repre_context: src_files, resource_files = self._get_source_files_with_frames() else: @@ -344,7 +328,7 @@ class ProjectPushRepreItem: udim_placeholder = "__udim__" src_files = [] resource_files = [] - template = self._repre_doc["data"]["template"] + template = self._repre_entity["attrib"]["template"] # Remove padding from 'udim' and 'frame' formatting keys # - "{frame:0>4}" -> "{frame}" for key in ("udim", "frame"): @@ -352,7 +336,7 @@ class ProjectPushRepreItem: replacement = "{{{}}}".format(key) template = re.sub(sub_part, replacement, template) - repre_context = self._repre_doc["context"] + repre_context = self._repre_entity["context"] fill_repre_context = copy.deepcopy(repre_context) if "frame" in fill_repre_context: fill_repre_context["frame"] = frame_placeholder @@ -373,7 +357,7 @@ class ProjectPushRepreItem: .replace(udim_placeholder, "(?P[0-9]+)") ) src_basename_regex = re.compile("^{}$".format(src_basename)) - for file_info in self._repre_doc["files"]: + for file_info in self._repre_entity["files"]: filepath_template = self._clean_path(file_info["path"]) filepath = self._clean_path( filepath_template.format(root=self._roots) @@ -405,8 +389,8 @@ class ProjectPushRepreItem: def _get_source_files(self): src_files = [] resource_files = [] - template = self._repre_doc["data"]["template"] - repre_context = self._repre_doc["context"] + template = self._repre_entity["attrib"]["template"] + repre_context = self._repre_entity["context"] fill_repre_context = copy.deepcopy(repre_context) fill_roots = fill_repre_context["root"] for root_name in tuple(fill_roots.keys()): @@ -415,7 +399,7 @@ class ProjectPushRepreItem: fill_repre_context) repre_path = self._clean_path(repre_path) src_dirpath = os.path.dirname(repre_path) - for file_info in self._repre_doc["files"]: + for file_info in self._repre_entity["files"]: filepath_template = self._clean_path(file_info["path"]) filepath = self._clean_path( filepath_template.format(root=self._roots)) @@ -448,18 +432,17 @@ class ProjectPushItemProcess: self._model = model self._item = item - self._src_asset_doc = None - self._src_subset_doc = None - self._src_version_doc = None + self._src_folder_entity = None + self._src_product_entity = None + self._src_version_entity = None self._src_repre_items = None - self._project_doc = None + self._project_entity = None self._anatomy = None - self._asset_doc = None - self._created_asset_doc = None + self._folder_entity = None self._task_info = None - self._subset_doc = None - self._version_doc = None + self._product_entity = None + self._version_entity = None self._product_type = None self._product_name = None @@ -492,12 +475,12 @@ class ProjectPushItemProcess: self._log_info("Source entities were found") self._fill_destination_project() self._log_info("Destination project was found") - self._fill_or_create_destination_asset() - self._log_info("Destination asset was determined") + self._fill_or_create_destination_folder() + self._log_info("Destination folder was determined") self._determine_product_type() self._determine_publish_template_name() self._determine_product_name() - self._make_sure_subset_exists() + self._make_sure_product_exists() self._make_sure_version_exists() self._log_info("Prerequirements were prepared") self._integrate_representations() @@ -562,8 +545,8 @@ class ProjectPushItemProcess: src_project_name = self._item.src_project_name src_version_id = self._item.src_version_id - project_doc = get_project(src_project_name) - if not project_doc: + project_entity = ayon_api.get_project(src_project_name) + if not project_entity: self._status.set_failed( f"Source project \"{src_project_name}\" was not found" ) @@ -576,41 +559,47 @@ class ProjectPushItemProcess: self._log_debug(f"Project '{src_project_name}' found") - version_doc = get_version_by_id(src_project_name, src_version_id) - if not version_doc: + version_entity = ayon_api.get_version_by_id( + src_project_name, src_version_id + ) + if not version_entity: self._status.set_failed(( f"Source version with id \"{src_version_id}\"" f" was not found in project \"{src_project_name}\"" )) raise PushToProjectError(self._status.fail_reason) - product_id = version_doc["parent"] - subset_doc = get_subset_by_id(src_project_name, product_id) - if not subset_doc: + product_id = version_entity["productId"] + product_entity = ayon_api.get_product_by_id( + src_project_name, product_id + ) + if not product_entity: self._status.set_failed(( f"Could find product with id \"{product_id}\"" f" in project \"{src_project_name}\"" )) raise PushToProjectError(self._status.fail_reason) - asset_id = subset_doc["parent"] - asset_doc = get_asset_by_id(src_project_name, asset_id) - if not asset_doc: + folder_id = product_entity["folderId"] + folder_entity = ayon_api.get_folder_by_id( + src_project_name, folder_id, own_attributes=True + ) + if not folder_entity: self._status.set_failed(( - f"Could find asset with id \"{asset_id}\"" + f"Could find folder with id \"{folder_id}\"" f" in project \"{src_project_name}\"" )) raise PushToProjectError(self._status.fail_reason) anatomy = Anatomy(src_project_name) - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( src_project_name, - version_ids=[src_version_id] + version_ids={src_version_id} ) repre_items = [ - ProjectPushRepreItem(repre_doc, anatomy.roots) - for repre_doc in repre_docs + ProjectPushRepreItem(repre_entity, anatomy.roots) + for repre_entity in repre_entities ] self._log_debug(( f"Found {len(repre_items)} representations on" @@ -623,17 +612,17 @@ class ProjectPushItemProcess: ) raise PushToProjectError(self._status.fail_reason) - self._src_asset_doc = asset_doc - self._src_subset_doc = subset_doc - self._src_version_doc = version_doc + self._src_folder_entity = folder_entity + self._src_product_entity = product_entity + self._src_version_entity = version_entity self._src_repre_items = repre_items def _fill_destination_project(self): # --- Destination entities --- dst_project_name = self._item.dst_project_name # Validate project existence - dst_project_doc = get_project(dst_project_name) - if not dst_project_doc: + dst_project_entity = ayon_api.get_project(dst_project_name) + if not dst_project_entity: self._status.set_failed( f"Destination project '{dst_project_name}' was not found" ) @@ -642,53 +631,46 @@ class ProjectPushItemProcess: self._log_debug( f"Destination project '{dst_project_name}' found" ) - self._project_doc = dst_project_doc - self._anatomy = Anatomy(dst_project_name) + self._project_entity = dst_project_entity + self._anatomy = Anatomy( + dst_project_name, + project_entity=dst_project_entity + ) self._project_settings = get_project_settings( self._item.dst_project_name ) - def _create_asset( + def _create_folder( self, - src_asset_doc, - project_doc, - parent_asset_doc, - asset_name + src_folder_entity, + project_entity, + parent_folder_entity, + folder_name ): parent_id = None - parents = [] - tools = [] - if parent_asset_doc: - parent_id = parent_asset_doc["_id"] - parents = list(parent_asset_doc["data"]["parents"]) - parents.append(parent_asset_doc["name"]) - _tools = parent_asset_doc["data"].get("tools_env") - if _tools: - tools = list(_tools) + if parent_folder_entity: + parent_id = parent_folder_entity["id"] - asset_name_low = asset_name.lower() - other_asset_docs = get_assets( - project_doc["name"], fields=["_id", "name", "data.visualParent"] + folder_name_low = folder_name.lower() + other_folder_entities = ayon_api.get_folders( + project_entity["name"], + parent_ids=[parent_id], + fields={"id", "name"} ) - for other_asset_doc in other_asset_docs: - other_name = other_asset_doc["name"] - other_parent_id = other_asset_doc["data"].get("visualParent") - if other_name.lower() != asset_name_low: + for other_folder_entity in other_folder_entities: + other_name = other_folder_entity["name"] + if other_name.lower() != folder_name_low: continue - if other_parent_id != parent_id: - self._status.set_failed(( - f"Asset with name \"{other_name}\" already" - " exists in different hierarchy." - )) - raise PushToProjectError(self._status.fail_reason) - self._log_debug(( - f"Found already existing asset with name \"{other_name}\"" - f" which match requested name \"{asset_name}\"" + f"Found already existing folder with name \"{other_name}\"" + f" which match requested name \"{folder_name}\"" )) - return get_asset_by_id(project_doc["name"], other_asset_doc["_id"]) + return ayon_api.get_folder_by_id( + project_entity["name"], other_folder_entity["id"] + ) + # TODO should we hard pass attribute values? data_keys = ( "clipIn", "clipOut", @@ -701,103 +683,113 @@ class ProjectPushItemProcess: "fps", "pixelAspect", ) - asset_data = { - "visualParent": parent_id, - "parents": parents, - "tasks": {}, - "tools_env": tools - } - src_asset_data = src_asset_doc["data"] - for key in data_keys: - if key in src_asset_data: - asset_data[key] = src_asset_data[key] + new_folder_attrib = {} + src_attrib = src_folder_entity["attrib"] + for attr_name, attr_value in src_attrib.items(): + if attr_name in data_keys: + new_folder_attrib[attr_name] = attr_value - asset_doc = new_asset_document( - asset_name, - project_doc["_id"], - parent_id, - parents, - data=asset_data + new_folder_name = ayon_api.slugify_string(folder_name) + folder_label = None + if new_folder_name != folder_name: + folder_label = folder_name + + # TODO find out how to define folder type + folder_entity = new_folder_entity( + folder_name, + "Folder", + parent_id=parent_id, + attribs=new_folder_attrib ) + if folder_label: + folder_entity["label"] = folder_label + self._operations.create_entity( - project_doc["name"], - asset_doc["type"], - asset_doc + project_entity["name"], + "folder", + folder_entity ) self._log_info( - f"Creating new asset with name \"{asset_name}\"" + f"Creating new folder with name \"{folder_name}\"" ) - self._created_asset_doc = asset_doc - return asset_doc + # Calculate path for usage in rest of logic + parent_path = "" + if parent_folder_entity: + parent_path = parent_folder_entity["path"] + folder_entity["path"] = "/".join([parent_path, folder_name]) + return folder_entity - def _fill_or_create_destination_asset(self): + 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 + dst_task_name_low = dst_task_name.lower() new_folder_name = self._item.new_folder_name if not dst_folder_id and not new_folder_name: self._status.set_failed( - "Push item does not have defined destination asset" + "Push item does not have defined destination folder" ) raise PushToProjectError(self._status.fail_reason) - # Get asset document - parent_asset_doc = None + # Get folder entity + parent_folder_entity = None if dst_folder_id: - parent_asset_doc = get_asset_by_id( + parent_folder_entity = ayon_api.get_folder_by_id( self._item.dst_project_name, self._item.dst_folder_id ) - if not parent_asset_doc: + if not parent_folder_entity: self._status.set_failed( - f"Could find asset with id \"{dst_folder_id}\"" + f"Could find folder with id \"{dst_folder_id}\"" f" in project \"{dst_project_name}\"" ) raise PushToProjectError(self._status.fail_reason) if not new_folder_name: - asset_doc = parent_asset_doc + folder_entity = parent_folder_entity else: - asset_doc = self._create_asset( - self._src_asset_doc, - self._project_doc, - parent_asset_doc, + folder_entity = self._create_folder( + self._src_folder_entity, + self._project_entity, + parent_folder_entity, new_folder_name ) - self._asset_doc = asset_doc + self._folder_entity = folder_entity if not dst_task_name: self._task_info = {} return - asset_path_parts = list(asset_doc["data"]["parents"]) - asset_path_parts.append(asset_doc["name"]) - asset_path = "/".join(asset_path_parts) - asset_tasks = asset_doc.get("data", {}).get("tasks") or {} - task_info = asset_tasks.get(dst_task_name) + folder_path = folder_entity["path"] + folder_tasks = { + task_entity["name"].lower(): task_entity + for task_entity in ayon_api.get_tasks( + dst_project_name, folder_ids=[folder_entity["id"]] + ) + } + task_info = folder_tasks.get(dst_task_name_low) if not task_info: self._status.set_failed( f"Could find task with name \"{dst_task_name}\"" - f" on asset \"{asset_path}\"" + f" on folder \"{folder_path}\"" f" in project \"{dst_project_name}\"" ) raise PushToProjectError(self._status.fail_reason) - # Create copy of task info to avoid changing data in asset document + # Create copy of task info to avoid changing data in task entity task_info = copy.deepcopy(task_info) task_info["name"] = dst_task_name # Fill rest of task information based on task type - task_type = task_info["type"] - task_type_info = self._project_doc["config"]["tasks"].get( - task_type, {}) + task_type_name = task_info["type"] + task_types_by_name = { + 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_info.update(task_type_info) self._task_info = task_info def _determine_product_type(self): - subset_doc = self._src_subset_doc - product_type = subset_doc["data"].get("family") - families = subset_doc["data"].get("families") - if not product_type and families: - product_type = families[0] - + product_entity = self._src_product_entity + product_type = product_entity["productType"] if not product_type: self._status.set_failed( "Couldn't figure out product type from source product" @@ -826,12 +818,16 @@ class ProjectPushItemProcess: def _determine_product_name(self): product_type = self._product_type - asset_doc = self._asset_doc task_info = self._task_info + task_name = task_type = None + if task_info: + task_name = task_info["name"] + task_type = task_info["type"] + product_name = get_product_name( self._item.dst_project_name, - asset_doc, - task_info.get("name"), + task_name, + task_type, self.host_name, product_type, self._item.variant, @@ -842,91 +838,103 @@ class ProjectPushItemProcess: ) self._product_name = product_name - def _make_sure_subset_exists(self): + def _make_sure_product_exists(self): project_name = self._item.dst_project_name - asset_id = self._asset_doc["_id"] + folder_id = self._folder_entity["id"] product_name = self._product_name product_type = self._product_type - subset_doc = get_subset_by_name(project_name, product_name, asset_id) - if subset_doc: - self._subset_doc = subset_doc - return subset_doc - - data = { - "families": [product_type] - } - subset_doc = new_subset_document( - product_name, product_type, asset_id, data + product_entity = ayon_api.get_product_by_name( + project_name, product_name, folder_id ) - self._operations.create_entity(project_name, "subset", subset_doc) - self._subset_doc = subset_doc + if product_entity: + self._product_entity = product_entity + return product_entity + + product_entity = new_product_entity( + product_name, + product_type, + folder_id, + ) + self._operations.create_entity( + project_name, "product", product_entity + ) + self._product_entity = product_entity def _make_sure_version_exists(self): """Make sure version document exits in database.""" project_name = self._item.dst_project_name version = self._item.dst_version - src_version_doc = self._src_version_doc - subset_doc = self._subset_doc - product_id = subset_doc["_id"] - src_data = src_version_doc["data"] - families = subset_doc["data"].get("families") - if not families: - families = [subset_doc["data"]["family"]] + src_version_entity = self._src_version_entity + product_entity = self._product_entity + product_id = product_entity["id"] + product_type = product_entity["productType"] + src_attrib = src_version_entity["attrib"] + + dst_attrib = {} + for key in { + "productType", + "productTypes", + "families", + "fps", + "pixelAspect", + "clipIn", + "clipOut", + "frameStart", + "frameEnd", + "handleStart", + "handleEnd", + "resolutionWidth", + "resolutionHeight", + "colorSpace", + "source", + "comment", + "description", + "intent", + }: + if key in src_attrib: + dst_attrib[key] = src_attrib[key] - version_data = { - "families": list(families), - "fps": src_data.get("fps"), - "source": src_data.get("source"), - "machine": socket.gethostname(), - "comment": self._item.comment or "", - "author": get_ayon_username(), - "time": get_formatted_current_time(), - } if version is None: - last_version_doc = get_last_version_by_subset_id( + last_version_entity = ayon_api.get_last_version_by_product_id( project_name, product_id ) - if last_version_doc: - version = int(last_version_doc["name"]) + 1 + 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["name"], task_type=self._task_info["type"], - product_type=families[0], - product_name=subset_doc["name"] + product_type=product_type, + product_name=product_entity["name"] ) - existing_version_doc = get_version_by_name( + existing_version_entity = ayon_api.get_version_by_name( project_name, version, product_id ) # Update existing version - if existing_version_doc: - version_doc = new_version_doc( - version, product_id, version_data, existing_version_doc["_id"] + if existing_version_entity: + self._operations.update_entity( + project_name, + "version", + existing_version_entity["id"], + {"attrib": dst_attrib} ) - update_data = prepare_version_update_data( - existing_version_doc, version_doc - ) - if update_data: - self._operations.update_entity( - project_name, - "version", - existing_version_doc["_id"], - update_data - ) - self._version_doc = version_doc - + existing_version_entity["attrib"].update(dst_attrib) + self._version_entity = existing_version_entity return - version_doc = new_version_doc( - version, product_id, version_data + version_entity = new_version_entity( + version, + product_id, + attribs=dst_attrib, ) - self._operations.create_entity(project_name, "version", version_doc) - - self._version_doc = version_doc + self._operations.create_entity( + project_name, "version", version_entity + ) + self._version_entity = version_entity def _integrate_representations(self): try: @@ -937,21 +945,21 @@ class ProjectPushItemProcess: raise def _real_integrate_representations(self): - version_doc = self._version_doc - version_id = version_doc["_id"] - existing_repres = get_representations( + version_entity = self._version_entity + version_id = version_entity["id"] + existing_repres = ayon_api.get_representations( self._item.dst_project_name, - version_ids=[version_id] + version_ids={version_id} ) existing_repres_by_low_name = { - repre_doc["name"].lower(): repre_doc - for repre_doc in existing_repres + repre_entity["name"].lower(): repre_entity + for repre_entity in existing_repres } template_name = self._template_name anatomy = self._anatomy formatting_data = get_template_data( - self._project_doc, - self._asset_doc, + self._project_entity, + self._folder_entity, self._task_info.get("name"), self.host_name ) @@ -962,7 +970,7 @@ class ProjectPushItemProcess: "name": self._product_name, "type": self._product_type, }, - "version": version_doc["name"] + "version": version_entity["version"] }) path_template = anatomy.templates[template_name]["path"].replace( @@ -992,8 +1000,8 @@ class ProjectPushItemProcess: ): processed_repre_items = [] for repre_item in self._src_repre_items: - repre_doc = repre_item.repre_doc - repre_name = repre_doc["name"] + repre_entity = repre_item.repre_entity + repre_name = repre_entity["name"] repre_format_data = copy.deepcopy(formatting_data) repre_format_data["representation"] = repre_name for src_file in repre_item.src_files: @@ -1002,7 +1010,7 @@ class ProjectPushItemProcess: break # Re-use 'output' from source representation - repre_output_name = repre_doc["context"].get("output") + repre_output_name = repre_entity["context"].get("output") if repre_output_name is not None: repre_format_data["output"] = repre_output_name @@ -1059,34 +1067,24 @@ class ProjectPushItemProcess: path_template, existing_repres_by_low_name ): - addons_manager = AddonsManager() - sync_server_module = addons_manager.get("sync_server") - if sync_server_module is None or not sync_server_module.enabled: - sites = [{ - "name": "studio", - "created_dt": datetime.datetime.now() - }] - else: - sites = sync_server_module.compute_resource_sync_sites( - project_name=self._item.dst_project_name - ) - added_repre_names = set() for item in processed_repre_items: (repre_item, repre_filepaths, repre_context, published_path) = item - repre_name = repre_item.repre_doc["name"] + repre_name = repre_item.repre_entity["name"] added_repre_names.add(repre_name.lower()) - new_repre_data = { + new_repre_attributes = { "path": published_path, "template": path_template } new_repre_files = [] for (path, rootless_path) in repre_filepaths: new_repre_files.append({ + "id": create_entity_id(), + "name": os.path.basename(rootless_path), "path": rootless_path, "size": os.path.getsize(path), "hash": source_hash(path), - "sites": sites + "hash_type": "op3", }) existing_repre = existing_repres_by_low_name.get( @@ -1094,41 +1092,51 @@ class ProjectPushItemProcess: ) entity_id = None if existing_repre: - entity_id = existing_repre["_id"] - new_repre_doc = new_representation_doc( + entity_id = existing_repre["id"] + repre_entity = new_representation_entity( repre_name, version_id, - repre_context, - data=new_repre_data, + new_repre_files, + data={"context": repre_context}, + attribs=new_repre_attributes, entity_id=entity_id ) - new_repre_doc["files"] = new_repre_files if not existing_repre: self._operations.create_entity( self._item.dst_project_name, - new_repre_doc["type"], - new_repre_doc + "representation", + repre_entity ) else: - update_data = prepare_representation_update_data( - existing_repre, new_repre_doc - ) - if update_data: + changes = {} + for key, value in repre_entity.items(): + if key == "attrib": + continue + if value != existing_repre.get(key): + changes[key] = value + attrib_changes = {} + for key, value in repre_entity["attrib"].items(): + if value != existing_repre["attrib"].get(key): + attrib_changes[key] = value + if attrib_changes: + changes["attrib"] = attrib_changes + + if changes: self._operations.update_entity( self._item.dst_project_name, - new_repre_doc["type"], - new_repre_doc["_id"], - update_data + "representation", + entity_id, + changes ) existing_repre_names = set(existing_repres_by_low_name.keys()) for repre_name in (existing_repre_names - added_repre_names): - repre_doc = existing_repres_by_low_name[repre_name] + repre_entity = existing_repres_by_low_name[repre_name] self._operations.update_entity( self._item.dst_project_name, - repre_doc["type"], - repre_doc["_id"], - {"type": "archived_representation"} + "representation", + repre_entity["id"], + {"active": False} ) 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 4d39075dc3..bc2fc6bf96 100644 --- a/client/ayon_core/tools/push_to_project/ui/window.py +++ b/client/ayon_core/tools/push_to_project/ui/window.py @@ -158,7 +158,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): main_thread_timer.timeout.connect(self._on_main_thread_timer) show_timer.timeout.connect(self._on_show_timer) user_input_changed_timer.timeout.connect(self._on_user_input_timer) - folder_name_input.textChanged.connect(self._on_new_asset_change) + folder_name_input.textChanged.connect(self._on_new_folder_change) variant_input.textChanged.connect(self._on_variant_change) comment_input.textChanged.connect(self._on_comment_change) @@ -169,7 +169,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): controller.register_event_callback( "new_folder_name.changed", - self._on_controller_new_asset_change + self._on_controller_new_folder_change ) controller.register_event_callback( "variant.changed", self._on_controller_variant_change @@ -291,7 +291,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self.refresh() - def _on_new_asset_change(self, text): + def _on_new_folder_change(self, text): self._new_folder_name_input_text = text self._user_input_changed_timer.start() @@ -319,7 +319,7 @@ class PushToContextSelectWindow(QtWidgets.QWidget): self._comment_input_text = None self._controller.set_user_value_comment(comment) - def _on_controller_new_asset_change(self, event): + def _on_controller_new_folder_change(self, event): folder_name = event["new_folder_name"] if ( self._new_folder_name_input_text is None diff --git a/client/ayon_core/tools/sceneinventory/delegates.py b/client/ayon_core/tools/sceneinventory/delegates.py index 1f8bb81835..2126fa1cbe 100644 --- a/client/ayon_core/tools/sceneinventory/delegates.py +++ b/client/ayon_core/tools/sceneinventory/delegates.py @@ -1,9 +1,7 @@ import numbers -from ayon_core.client import ( - get_versions, - get_hero_versions, -) +import ayon_api + from ayon_core.pipeline import HeroVersionType from ayon_core.tools.utils.models import TreeModel from ayon_core.tools.utils.lib import format_version @@ -27,7 +25,7 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): def displayText(self, value, locale): if isinstance(value, HeroVersionType): - return format_version(value, True) + return format_version(value) if not isinstance(value, numbers.Integral): # For cases where no version is resolved like NOT FOUND cases # where a representation might not exist in current database @@ -113,71 +111,35 @@ class VersionDelegate(QtWidgets.QStyledItemDelegate): # Current value of the index item = index.data(TreeModel.ItemRole) value = index.data(QtCore.Qt.DisplayRole) - if item["version_document"]["type"] != "hero_version": - assert isinstance(value, numbers.Integral), ( - "Version is not integer" - ) project_name = self.get_project_name() # Add all available versions to the editor - parent_id = item["version_document"]["parent"] - version_docs = [ - version_doc - for version_doc in sorted( - get_versions(project_name, subset_ids=[parent_id]), - key=lambda item: item["name"] - ) - if version_doc["data"].get("active", True) - ] - - hero_versions = list( - get_hero_versions( - project_name, - subset_ids=[parent_id], - fields=["name", "data.tags", "version_id"] - ) - ) - hero_version_doc = None - if hero_versions: - hero_version_doc = hero_versions[0] - - doc_for_hero_version = None + product_id = item["version_entity"]["productId"] + version_entities = list(sorted( + ayon_api.get_versions( + project_name, product_ids={product_id}, active=True + ), + key=lambda item: abs(item["version"]) + )) selected = None items = [] - for version_doc in version_docs: - version_tags = version_doc["data"].get("tags") or [] - if "deleted" in version_tags: - continue + is_hero_version = value < 0 + for version_entity in version_entities: + version = version_entity["version"] + label = format_version(version) + item = QtGui.QStandardItem(label) + item.setData(version_entity, QtCore.Qt.UserRole) + items.append(item) if ( - hero_version_doc - and doc_for_hero_version is None - and hero_version_doc["version_id"] == version_doc["_id"] + version == value + or is_hero_version and version < 0 ): - doc_for_hero_version = version_doc - - label = format_version(version_doc["name"]) - item = QtGui.QStandardItem(label) - item.setData(version_doc, QtCore.Qt.UserRole) - items.append(item) - - if version_doc["name"] == value: selected = item - if hero_version_doc and doc_for_hero_version: - version_name = doc_for_hero_version["name"] - label = format_version(version_name, True) - if isinstance(value, HeroVersionType): - index = len(version_docs) - hero_version_doc["name"] = HeroVersionType(version_name) - - item = QtGui.QStandardItem(label) - item.setData(hero_version_doc, QtCore.Qt.UserRole) - items.append(item) - # Reverse items so latest versions be upper - items = list(reversed(items)) + items.reverse() for item in items: editor.model().appendRow(item) diff --git a/client/ayon_core/tools/sceneinventory/model.py b/client/ayon_core/tools/sceneinventory/model.py index 18fc56db0b..e53b6aa4c3 100644 --- a/client/ayon_core/tools/sceneinventory/model.py +++ b/client/ayon_core/tools/sceneinventory/model.py @@ -1,24 +1,15 @@ -import collections import re import logging import uuid -import copy from collections import defaultdict +import ayon_api from qtpy import QtCore, QtGui import qtawesome -from ayon_core.client import ( - get_assets, - get_subsets, - get_versions, - get_last_version_by_subset_id, - get_representations, -) from ayon_core.pipeline import ( get_current_project_name, - schema, HeroVersionType, ) from ayon_core.style import get_default_entity_icon_color @@ -249,29 +240,29 @@ class InventoryModel(TreeModel): not_found_ids.append(repre_id) continue - version = versions_by_id.get(representation["parent"]) - if not version: + version_entity = versions_by_id.get(representation["versionId"]) + if not version_entity: not_found["version"].extend(group_containers) not_found_ids.append(repre_id) continue - product = products_by_id.get(version["parent"]) - if not product: + product_entity = products_by_id.get(version_entity["productId"]) + if not product_entity: not_found["product"].extend(group_containers) not_found_ids.append(repre_id) continue - folder = folders_by_id.get(product["parent"]) - if not folder: + folder_entity = folders_by_id.get(product_entity["folderId"]) + if not folder_entity: not_found["folder"].extend(group_containers) not_found_ids.append(repre_id) continue group_dict.update({ "representation": representation, - "version": version, - "subset": product, - "asset": folder + "version": version_entity, + "product": product_entity, + "folder": folder_entity }) for _repre_id in not_found_ids: @@ -308,43 +299,34 @@ class InventoryModel(TreeModel): for repre_id, group_dict in sorted(grouped.items()): group_containers = group_dict["containers"] - representation = group_dict["representation"] - version = group_dict["version"] - subset = group_dict["subset"] - asset = group_dict["asset"] + repre_entity = group_dict["representation"] + version_entity = group_dict["version"] + folder_entity = group_dict["folder"] + product_entity = group_dict["product"] - # Get product type - maj_version, _ = schema.get_schema_version(subset["schema"]) - if maj_version < 3: - src_doc = version - else: - src_doc = subset - - product_type = src_doc["data"].get("family") - if not product_type: - families = src_doc["data"].get("families") - if families: - product_type = families[0] + product_type = product_entity["productType"] # Store the highest available version so the model can know # whether current version is currently up-to-date. - highest_version = get_last_version_by_subset_id( - project_name, version["parent"] + highest_version = ayon_api.get_last_version_by_product_id( + project_name, version_entity["productId"] ) # create the group header group_node = Item() group_node["Name"] = "{}_{}: ({})".format( - asset["name"], subset["name"], representation["name"] + folder_entity["name"], + product_entity["name"], + repre_entity["name"] ) group_node["representation"] = repre_id - group_node["version"] = version["name"] - group_node["highest_version"] = highest_version["name"] + group_node["version"] = version_entity["version"] + group_node["highest_version"] = highest_version["version"] group_node["productType"] = product_type or "" group_node["productTypeIcon"] = product_type_icon group_node["count"] = len(group_containers) group_node["isGroupNode"] = True - group_node["group"] = subset["data"].get("subsetGroup") + group_node["group"] = product_entity["attrib"].get("productGroup") # Site sync specific data progress = progress_by_id[repre_id] @@ -359,7 +341,8 @@ class InventoryModel(TreeModel): item_node.update(container) # store the current version on the item - item_node["version"] = version["name"] + item_node["version"] = version_entity["version"] + item_node["version_entity"] = version_entity # Remapping namespace to item name. # Noted that the name key is capital "N", by doing this, we @@ -404,73 +387,50 @@ class InventoryModel(TreeModel): if not filtered_repre_ids: return output - repre_docs = get_representations(project_name, repre_ids) + repre_entities = ayon_api.get_representations(project_name, repre_ids) repres_by_id.update({ - repre_doc["_id"]: repre_doc - for repre_doc in repre_docs + repre_entity["id"]: repre_entity + for repre_entity in repre_entities }) version_ids = { - repre_doc["parent"] for repre_doc in repres_by_id.values() + repre_entity["versionId"] + for repre_entity in repres_by_id.values() } if not version_ids: return output - version_docs = get_versions(project_name, version_ids, hero=True) versions_by_id.update({ - version_doc["_id"]: version_doc - for version_doc in version_docs - }) - hero_versions_by_subversion_id = collections.defaultdict(list) - for version_doc in versions_by_id.values(): - if version_doc["type"] != "hero_version": - continue - subversion = version_doc["version_id"] - hero_versions_by_subversion_id[subversion].append(version_doc) - - if hero_versions_by_subversion_id: - subversion_ids = set( - hero_versions_by_subversion_id.keys() + version_entity["id"]: version_entity + for version_entity in ayon_api.get_versions( + project_name, version_ids=version_ids ) - subversion_docs = get_versions(project_name, subversion_ids) - for subversion_doc in subversion_docs: - subversion_id = subversion_doc["_id"] - subversion_ids.discard(subversion_id) - h_version_docs = hero_versions_by_subversion_id[subversion_id] - for version_doc in h_version_docs: - version_doc["name"] = HeroVersionType( - subversion_doc["name"] - ) - version_doc["data"] = copy.deepcopy( - subversion_doc["data"] - ) - - for subversion_id in subversion_ids: - h_version_docs = hero_versions_by_subversion_id[subversion_id] - for version_doc in h_version_docs: - versions_by_id.pop(version_doc["_id"]) + }) product_ids = { - version_doc["parent"] - for version_doc in versions_by_id.values() + version_entity["productId"] + for version_entity in versions_by_id.values() } if not product_ids: return output - product_docs = get_subsets(project_name, product_ids) + products_by_id.update({ - product_doc["_id"]: product_doc - for product_doc in product_docs + product_entity["id"]: product_entity + for product_entity in ayon_api.get_products( + project_name, product_ids=product_ids + ) }) folder_ids = { - product_doc["parent"] - for product_doc in products_by_id.values() + product_entity["folderId"] + for product_entity in products_by_id.values() } if not folder_ids: return output - folder_docs = get_assets(project_name, folder_ids) folders_by_id.update({ - folder_doc["_id"]: folder_doc - for folder_doc in folder_docs + folder_entity["id"]: folder_entity + for folder_entity in ayon_api.get_folders( + project_name, folder_ids=folder_ids + ) }) return output diff --git a/client/ayon_core/tools/sceneinventory/models/site_sync.py b/client/ayon_core/tools/sceneinventory/models/site_sync.py index c7bc0b756d..7f09f2b25b 100644 --- a/client/ayon_core/tools/sceneinventory/models/site_sync.py +++ b/client/ayon_core/tools/sceneinventory/models/site_sync.py @@ -1,4 +1,5 @@ -from ayon_core.client import get_representations +import ayon_api + from ayon_core.addon import AddonsManager NOT_SET = object() @@ -69,14 +70,16 @@ class SiteSyncModel: project_name = self._controller.get_current_project_name() site_sync = self._get_sync_server_module() - repre_docs = get_representations(project_name, representation_ids) + repre_entities = ayon_api.get_representations( + project_name, representation_ids + ) active_site = self._get_active_site() remote_site = self._get_remote_site() - for repre_doc in repre_docs: - repre_output = output[repre_doc["_id"]] + for repre_entity in repre_entities: + repre_output = output[repre_entity["id"]] result = site_sync.get_progress_for_repre( - repre_doc, active_site, remote_site + repre_entity, active_site, remote_site ) repre_output["active_site"] = result[active_site] repre_output["remote_site"] = result[remote_site] diff --git a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py index 89c3b652e1..823a4a6631 100644 --- a/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py +++ b/client/ayon_core/tools/sceneinventory/switch_dialog/dialog.py @@ -1,18 +1,10 @@ import collections import logging +import ayon_api from qtpy import QtWidgets, QtCore import qtawesome -from ayon_core.client import ( - get_assets, - get_subset_by_name, - get_subsets, - get_versions, - get_hero_versions, - get_last_versions, - get_representations, -) from ayon_core.pipeline.load import ( discover_loader_plugins, switch_container, @@ -135,16 +127,16 @@ class SwitchAssetDialog(QtWidgets.QDialog): # first asset field, this also allows to see the placeholder value. accept_btn.setFocus() - self._folder_docs_by_id = {} - self._product_docs_by_id = {} - self._version_docs_by_id = {} - self._repre_docs_by_id = {} + self._folder_entities_by_id = {} + self._product_entities_by_id = {} + self._version_entities_by_id = {} + self._repre_entities_by_id = {} self._missing_folder_ids = set() self._missing_product_ids = set() self._missing_version_ids = set() self._missing_repre_ids = set() - self._missing_docs = False + self._missing_entities = False self._inactive_folder_ids = set() self._inactive_product_ids = set() @@ -245,10 +237,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): def find_last_versions(self, product_ids): project_name = self._project_name - return get_last_versions( + return ayon_api.get_last_versions( project_name, - subset_ids=product_ids, - fields=["_id", "parent", "type"] + product_ids, + fields={"id", "folderId", "version"} ) def _on_show_timer(self): @@ -265,124 +257,119 @@ class SwitchAssetDialog(QtWidgets.QDialog): } project_name = self._project_name - repres = list(get_representations( + repre_entities = list(ayon_api.get_representations( project_name, representation_ids=repre_ids, - archived=True, )) - repres_by_id = {str(repre["_id"]): repre for repre in repres} + repres_by_id = {r["id"]: r for r in repre_entities} - content_repre_docs_by_id = {} + content_repre_entities_by_id = {} inactive_repre_ids = set() missing_repre_ids = set() version_ids = set() for repre_id in repre_ids: - repre_doc = repres_by_id.get(repre_id) - if repre_doc is None: + repre_entity = repres_by_id.get(repre_id) + if repre_entity is None: missing_repre_ids.add(repre_id) - elif repres_by_id[repre_id]["type"] == "archived_representation": + elif not repres_by_id[repre_id]["active"]: inactive_repre_ids.add(repre_id) - version_ids.add(repre_doc["parent"]) + version_ids.add(repre_entity["versionId"]) else: - content_repre_docs_by_id[repre_id] = repre_doc - version_ids.add(repre_doc["parent"]) + content_repre_entities_by_id[repre_id] = repre_entity + version_ids.add(repre_entity["versionId"]) - version_docs = get_versions( + version_entities = ayon_api.get_versions( project_name, - version_ids=version_ids, - hero=True + version_ids=version_ids ) - content_version_docs_by_id = {} - for version_doc in version_docs: - version_id = version_doc["_id"] - content_version_docs_by_id[version_id] = version_doc + content_version_entities_by_id = {} + for version_entity in version_entities: + version_id = version_entity["id"] + content_version_entities_by_id[version_id] = version_entity missing_version_ids = set() product_ids = set() for version_id in version_ids: - version_doc = content_version_docs_by_id.get(version_id) - if version_doc is None: + version_entity = content_version_entities_by_id.get(version_id) + if version_entity is None: missing_version_ids.add(version_id) else: - product_ids.add(version_doc["parent"]) + product_ids.add(version_entity["productId"]) - product_docs = get_subsets( - project_name, subset_ids=product_ids, archived=True + product_entities = ayon_api.get_products( + project_name, product_ids=product_ids ) - product_docs_by_id = {sub["_id"]: sub for sub in product_docs} + product_entities_by_id = {p["id"]: p for p in product_entities} folder_ids = set() inactive_product_ids = set() missing_product_ids = set() - content_product_docs_by_id = {} + content_product_entities_by_id = {} for product_id in product_ids: - product_doc = product_docs_by_id.get(product_id) - if product_doc is None: + product_entity = product_entities_by_id.get(product_id) + if product_entity is None: missing_product_ids.add(product_id) - elif product_doc["type"] == "archived_subset": - folder_ids.add(product_doc["parent"]) - inactive_product_ids.add(product_id) else: - folder_ids.add(product_doc["parent"]) - content_product_docs_by_id[product_id] = product_doc + folder_ids.add(product_entity["folderId"]) + content_product_entities_by_id[product_id] = product_entity - folder_docs = get_assets( - project_name, asset_ids=folder_ids, archived=True + folder_entities = ayon_api.get_folders( + project_name, folder_ids=folder_ids, active=None ) - folder_docs_by_id = { - folder_doc["_id"]: folder_doc - for folder_doc in folder_docs + folder_entities_by_id = { + folder_entity["id"]: folder_entity + for folder_entity in folder_entities } missing_folder_ids = set() inactive_folder_ids = set() - content_folder_docs_by_id = {} + content_folder_entities_by_id = {} for folder_id in folder_ids: - folder_doc = folder_docs_by_id.get(folder_id) - if folder_doc is None: + folder_entity = folder_entities_by_id.get(folder_id) + if folder_entity is None: missing_folder_ids.add(folder_id) - elif folder_doc["type"] == "archived_asset": + elif not folder_entity["active"]: inactive_folder_ids.add(folder_id) else: - content_folder_docs_by_id[folder_id] = folder_doc + content_folder_entities_by_id[folder_id] = folder_entity # stash context values, works only for single representation init_folder_id = None init_product_name = None init_repre_name = None - if len(repres) == 1: - init_repre_doc = repres[0] - init_version_doc = content_version_docs_by_id.get( - init_repre_doc["parent"]) - init_product_doc = None - init_folder_doc = None - if init_version_doc: - init_product_doc = content_product_docs_by_id.get( - init_version_doc["parent"] + if len(repre_entities) == 1: + init_repre_entity = repre_entities[0] + init_version_entity = content_version_entities_by_id.get( + init_repre_entity["versionId"]) + init_product_entity = None + init_folder_entity = None + if init_version_entity: + init_product_entity = content_product_entities_by_id.get( + init_version_entity["productId"] ) - if init_product_doc: - init_folder_doc = content_folder_docs_by_id.get( - init_product_doc["parent"] + if init_product_entity: + init_folder_entity = content_folder_entities_by_id.get( + init_product_entity["folderId"] ) - if init_folder_doc: - init_repre_name = init_repre_doc["name"] - init_product_name = init_product_doc["name"] - init_folder_id = init_folder_doc["_id"] + if init_folder_entity: + init_repre_name = init_repre_entity["name"] + init_product_name = init_product_entity["name"] + init_folder_id = init_folder_entity["id"] self._init_folder_id = init_folder_id self._init_product_name = init_product_name self._init_repre_name = init_repre_name - self._folder_docs_by_id = content_folder_docs_by_id - self._product_docs_by_id = content_product_docs_by_id - self._version_docs_by_id = content_version_docs_by_id - self._repre_docs_by_id = content_repre_docs_by_id + self._folder_entities_by_id = content_folder_entities_by_id + self._product_entities_by_id = content_product_entities_by_id + self._version_entities_by_id = content_version_entities_by_id + self._repre_entities_by_id = content_repre_entities_by_id self._missing_folder_ids = missing_folder_ids self._missing_product_ids = missing_product_ids self._missing_version_ids = missing_version_ids self._missing_repre_ids = missing_repre_ids - self._missing_docs = ( + self._missing_entities = ( bool(missing_folder_ids) or bool(missing_version_ids) or bool(missing_product_ids) @@ -524,7 +511,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): and not selected_product_name and not selected_repre ): - return list(self._repre_docs_by_id.keys()) + return list(self._repre_entities_by_id.keys()) # Everything is selected # [x] [x] [x] @@ -571,68 +558,68 @@ class SwitchAssetDialog(QtWidgets.QDialog): self, folder_id, selected_product_name, selected_repre ): project_name = self._project_name - product_doc = get_subset_by_name( + product_entity = ayon_api.get_product_by_name( project_name, selected_product_name, folder_id, - fields=["_id"] + fields={"id"} ) - product_id = product_doc["_id"] + product_id = product_entity["id"] last_versions_by_product_id = self.find_last_versions([product_id]) - version_doc = last_versions_by_product_id.get(product_id) - if not version_doc: + version_entity = last_versions_by_product_id.get(product_id) + if not version_entity: return [] - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, - version_ids=[version_doc["_id"]], - representation_names=[selected_repre], - fields=["_id"] + version_ids={version_entity["id"]}, + representation_names={selected_repre}, + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_current_output_repre_ids_xxo(self, folder_id, product_name): project_name = self._project_name - product_doc = get_subset_by_name( + product_entity = ayon_api.get_product_by_name( project_name, product_name, folder_id, - fields=["_id"] + fields={"id"} ) - if not product_doc: + if not product_entity: return [] repre_names = set() - for repre_doc in self._repre_docs_by_id.values(): - repre_names.add(repre_doc["name"]) + for repre_entity in self._repre_entities_by_id.values(): + repre_names.add(repre_entity["name"]) # TODO where to take version ids? version_ids = [] - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_names=repre_names, version_ids=version_ids, - fields=["_id"] + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_current_output_repre_ids_xox(self, folder_id, selected_repre): product_names = { - product_doc["name"] - for product_doc in self._product_docs_by_id.values() + product_entity["name"] + for product_entity in self._product_entities_by_id.values() } project_name = self._project_name - product_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=[folder_id], - subset_names=product_names, - fields=["_id", "name"] + folder_ids=[folder_id], + product_names=product_names, + fields={"id", "name"} ) product_name_by_id = { - product_doc["_id"]: product_doc["name"] - for product_doc in product_docs + product_entity["id"]: product_entity["name"] + for product_entity in product_entities } product_ids = list(product_name_by_id.keys()) last_versions_by_product_id = self.find_last_versions(product_ids) @@ -640,35 +627,37 @@ class SwitchAssetDialog(QtWidgets.QDialog): for product_id, last_version in last_versions_by_product_id.items(): product_name = product_name_by_id[product_id] last_version_id_by_product_name[product_name] = ( - last_version["_id"] + last_version["id"] ) - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, version_ids=last_version_id_by_product_name.values(), - representation_names=[selected_repre], - fields=["_id"] + representation_names={selected_repre}, + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_current_output_repre_ids_xoo(self, folder_id): project_name = self._project_name repres_by_product_name = collections.defaultdict(set) - for repre_doc in self._repre_docs_by_id.values(): - version_doc = self._version_docs_by_id[repre_doc["parent"]] - product_doc = self._product_docs_by_id[version_doc["parent"]] - product_name = product_doc["name"] - repres_by_product_name[product_name].add(repre_doc["name"]) + for repre_entity in self._repre_entities_by_id.values(): + version_id = repre_entity["versionId"] + version_entity = self._version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = self._product_entities_by_id[product_id] + product_name = product_entity["name"] + repres_by_product_name[product_name].add(repre_entity["name"]) - product_docs = list(get_subsets( + product_entities = list(ayon_api.get_products( project_name, - asset_ids=[folder_id], - subset_names=repres_by_product_name.keys(), - fields=["_id", "name"] + folder_ids=[folder_id], + product_names=repres_by_product_name.keys(), + fields={"id", "name"} )) product_name_by_id = { - product_doc["_id"]: product_doc["name"] - for product_doc in product_docs + product_entity["id"]: product_entity["name"] + for product_entity in product_entities } product_ids = list(product_name_by_id.keys()) last_versions_by_product_id = self.find_last_versions(product_ids) @@ -676,7 +665,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): for product_id, last_version in last_versions_by_product_id.items(): product_name = product_name_by_id[product_id] last_version_id_by_product_name[product_name] = ( - last_version["_id"] + last_version["id"] ) repre_names_by_version_id = {} @@ -686,97 +675,103 @@ class SwitchAssetDialog(QtWidgets.QDialog): if version_id is not None: repre_names_by_version_id[version_id] = list(repre_names) - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, names_by_version_ids=repre_names_by_version_id, - fields=["_id"] + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_current_output_repre_ids_oxx( self, product_name, selected_repre ): project_name = self._project_name - product_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=self._folder_docs_by_id.keys(), - subset_names=[product_name], - fields=["_id"] + folder_ids=self._folder_entities_by_id.keys(), + product_names=[product_name], + fields={"id"} ) - product_ids = [product_doc["_id"] for product_doc in product_docs] + product_ids = { + product_entity["id"] for product_entity in product_entities + } last_versions_by_product_id = self.find_last_versions(product_ids) - last_version_ids = [ - last_version["_id"] + last_version_ids = { + last_version["id"] for last_version in last_versions_by_product_id.values() - ] - repre_docs = get_representations( + } + + repre_entities = ayon_api.get_representations( project_name, version_ids=last_version_ids, - representation_names=[selected_repre], - fields=["_id"] + representation_names={selected_repre}, + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_current_output_repre_ids_oxo(self, product_name): project_name = self._project_name - product_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=self._folder_docs_by_id.keys(), - subset_names=[product_name], - fields=["_id", "parent"] + folder_ids=self._folder_entities_by_id.keys(), + product_names={product_name}, + fields={"id", "folderId"} ) - product_docs_by_id = { - product_doc["_id"]: product_doc - for product_doc in product_docs + product_entities_by_id = { + product_entity["id"]: product_entity + for product_entity in product_entities } - if not product_docs: + if not product_entities_by_id: return list() last_versions_by_product_id = self.find_last_versions( - product_docs_by_id.keys() + product_entities_by_id.keys() ) product_id_by_version_id = {} for product_id, last_version in last_versions_by_product_id.items(): - version_id = last_version["_id"] + version_id = last_version["id"] product_id_by_version_id[version_id] = product_id if not product_id_by_version_id: return list() repre_names_by_folder_id = collections.defaultdict(set) - for repre_doc in self._repre_docs_by_id.values(): - version_doc = self._version_docs_by_id[repre_doc["parent"]] - product_doc = self._product_docs_by_id[version_doc["parent"]] - folder_doc = self._folder_docs_by_id[product_doc["parent"]] - folder_id = folder_doc["_id"] - repre_names_by_folder_id[folder_id].add(repre_doc["name"]) + for repre_entity in self._repre_entities_by_id.values(): + version_id = repre_entity["versionId"] + version_entity = self._version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = self._product_entities_by_id[product_id] + folder_id = product_entity["folderId"] + folder_entity = self._folder_entities_by_id[folder_id] + folder_id = folder_entity["id"] + repre_names_by_folder_id[folder_id].add(repre_entity["name"]) repre_names_by_version_id = {} for last_version_id, product_id in product_id_by_version_id.items(): - product_doc = product_docs_by_id[product_id] - folder_id = product_doc["parent"] + product_entity = product_entities_by_id[product_id] + folder_id = product_entity["folderId"] repre_names = repre_names_by_folder_id.get(folder_id) if not repre_names: continue repre_names_by_version_id[last_version_id] = repre_names - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, names_by_version_ids=repre_names_by_version_id, - fields=["_id"] + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_current_output_repre_ids_oox(self, selected_repre): project_name = self._project_name - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_names=[selected_repre], - version_ids=self._version_docs_by_id.keys(), - fields=["_id"] + version_ids=self._version_entities_by_id.keys(), + fields={"id"} ) - return [repre_doc["_id"] for repre_doc in repre_docs] + return {repre_entity["id"] for repre_entity in repre_entities} def _get_product_box_values(self): project_name = self._project_name @@ -784,18 +779,18 @@ class SwitchAssetDialog(QtWidgets.QDialog): if selected_folder_id: folder_ids = [selected_folder_id] else: - folder_ids = list(self._folder_docs_by_id.keys()) + folder_ids = list(self._folder_entities_by_id.keys()) - product_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=folder_ids, - fields=["parent", "name"] + folder_ids=folder_ids, + fields={"folderId", "name"} ) product_names_by_parent_id = collections.defaultdict(set) - for product_doc in product_docs: - product_names_by_parent_id[product_doc["parent"]].add( - product_doc["name"] + for product_entity in product_entities: + product_names_by_parent_id[product_entity["folderId"]].add( + product_entity["name"] ) possible_product_names = None @@ -824,15 +819,17 @@ class SwitchAssetDialog(QtWidgets.QDialog): # [ ] [ ] [?] if not selected_folder_id and not selected_product_name: # Find all representations of selection's products - possible_repres = get_representations( + possible_repres = ayon_api.get_representations( project_name, - version_ids=self._version_docs_by_id.keys(), - fields=["parent", "name"] + version_ids=self._version_entities_by_id.keys(), + fields={"versionId", "name"} ) possible_repres_by_parent = collections.defaultdict(set) for repre in possible_repres: - possible_repres_by_parent[repre["parent"]].add(repre["name"]) + possible_repres_by_parent[repre["versionId"]].add( + repre["name"] + ) output_repres = None for repre_names in possible_repres_by_parent.values(): @@ -848,44 +845,44 @@ class SwitchAssetDialog(QtWidgets.QDialog): # [x] [x] [?] if selected_folder_id and selected_product_name: - product_doc = get_subset_by_name( + product_entity = ayon_api.get_product_by_name( project_name, selected_product_name, selected_folder_id, - fields=["_id"] + fields={"id"} ) - product_id = product_doc["_id"] + product_id = product_entity["id"] last_versions_by_product_id = self.find_last_versions([product_id]) - version_doc = last_versions_by_product_id.get(product_id) - repre_docs = get_representations( + version_entity = last_versions_by_product_id.get(product_id) + repre_entities = ayon_api.get_representations( project_name, - version_ids=[version_doc["_id"]], - fields=["name"] + version_ids={version_entity["id"]}, + fields={"name"} ) - return [ - repre_doc["name"] - for repre_doc in repre_docs - ] + return { + repre_entity["name"] + for repre_entity in repre_entities + } # [x] [ ] [?] # If only folder is selected if selected_folder_id: # Filter products by names from content product_names = { - product_doc["name"] - for product_doc in self._product_docs_by_id.values() + product_entity["name"] + for product_entity in self._product_entities_by_id.values() } - product_docs = get_subsets( + product_entities = ayon_api.get_products( project_name, - asset_ids=[selected_folder_id], - subset_names=product_names, - fields=["_id"] + folder_ids={selected_folder_id}, + product_names=product_names, + fields={"id"} ) product_ids = { - product_doc["_id"] - for product_doc in product_docs + product_entity["id"] + for product_entity in product_entities } if not product_ids: return list() @@ -895,24 +892,24 @@ class SwitchAssetDialog(QtWidgets.QDialog): for product_id, last_version in ( last_versions_by_product_id.items() ): - version_id = last_version["_id"] + version_id = last_version["id"] product_id_by_version_id[version_id] = product_id if not product_id_by_version_id: return list() - repre_docs = list(get_representations( + repre_entities = list(ayon_api.get_representations( project_name, version_ids=product_id_by_version_id.keys(), - fields=["name", "parent"] + fields={"name", "versionId"} )) - if not repre_docs: + if not repre_entities: return list() repre_names_by_parent = collections.defaultdict(set) - for repre_doc in repre_docs: - repre_names_by_parent[repre_doc["parent"]].add( - repre_doc["name"] + for repre_entity in repre_entities: + repre_names_by_parent[repre_entity["versionId"]].add( + repre_entity["name"] ) available_repres = None @@ -926,46 +923,46 @@ class SwitchAssetDialog(QtWidgets.QDialog): return list(available_repres) # [ ] [x] [?] - product_docs = list(get_subsets( + product_entities = list(ayon_api.get_products( project_name, - asset_ids=self._folder_docs_by_id.keys(), - subset_names=[selected_product_name], - fields=["_id", "parent"] + folder_ids=self._folder_entities_by_id.keys(), + product_names=[selected_product_name], + fields={"id", "folderId"} )) - if not product_docs: + if not product_entities: return list() - product_docs_by_id = { - product_doc["_id"]: product_doc - for product_doc in product_docs + product_entities_by_id = { + product_entity["id"]: product_entity + for product_entity in product_entities } last_versions_by_product_id = self.find_last_versions( - product_docs_by_id.keys() + product_entities_by_id.keys() ) product_id_by_version_id = {} for product_id, last_version in last_versions_by_product_id.items(): - version_id = last_version["_id"] + version_id = last_version["id"] product_id_by_version_id[version_id] = product_id if not product_id_by_version_id: return list() - repre_docs = list( - get_representations( + repre_entities = list( + ayon_api.get_representations( project_name, version_ids=product_id_by_version_id.keys(), - fields=["name", "parent"] + fields={"name", "versionId"} ) ) - if not repre_docs: + if not repre_entities: return list() repre_names_by_folder_id = collections.defaultdict(set) - for repre_doc in repre_docs: - product_id = product_id_by_version_id[repre_doc["parent"]] - folder_id = product_docs_by_id[product_id]["parent"] - repre_names_by_folder_id[folder_id].add(repre_doc["name"]) + for repre_entity in repre_entities: + product_id = product_id_by_version_id[repre_entity["versionId"]] + folder_id = product_entities_by_id[product_id]["folderId"] + repre_names_by_folder_id[folder_id].add(repre_entity["name"]) available_repres = None for repre_names in repre_names_by_folder_id.values(): @@ -981,7 +978,7 @@ class SwitchAssetDialog(QtWidgets.QDialog): selected_folder_id = self._folders_field.get_selected_folder_id() if ( selected_folder_id is None - and (self._missing_docs or self._inactive_folder_ids) + and (self._missing_entities or self._inactive_folder_ids) ): validation_state.folder_ok = False @@ -1003,17 +1000,17 @@ class SwitchAssetDialog(QtWidgets.QDialog): # [x] [ ] [?] project_name = self._project_name - product_docs = get_subsets( - project_name, asset_ids=[selected_folder_id], fields=["name"] + product_entities = ayon_api.get_products( + project_name, folder_ids=[selected_folder_id], fields={"name"} ) product_names = set( - product_doc["name"] - for product_doc in product_docs + product_entity["name"] + for product_entity in product_entities ) - for product_doc in self._product_docs_by_id.values(): - if product_doc["name"] not in product_names: + for product_entity in self._product_entities_by_id.values(): + if product_entity["name"] not in product_names: validation_state.product_ok = False break @@ -1043,49 +1040,49 @@ class SwitchAssetDialog(QtWidgets.QDialog): selected_folder_id is not None and selected_product_name is not None ): - product_doc = get_subset_by_name( + product_entity = ayon_api.get_product_by_name( project_name, selected_product_name, selected_folder_id, - fields=["_id"] + fields={"id"} ) - product_id = product_doc["_id"] + product_id = product_entity["id"] last_versions_by_product_id = self.find_last_versions([product_id]) last_version = last_versions_by_product_id.get(product_id) if not last_version: validation_state.repre_ok = False return - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, - version_ids=[last_version["_id"]], - fields=["name"] + version_ids={last_version["id"]}, + fields={"name"} ) repre_names = set( - repre_doc["name"] - for repre_doc in repre_docs + repre_entity["name"] + for repre_entity in repre_entities ) - for repre_doc in self._repre_docs_by_id.values(): - if repre_doc["name"] not in repre_names: + for repre_entity in self._repre_entities_by_id.values(): + if repre_entity["name"] not in repre_names: validation_state.repre_ok = False break return # [x] [ ] [ ] if selected_folder_id is not None: - product_docs = list(get_subsets( + product_entities = list(ayon_api.get_products( project_name, - asset_ids=[selected_folder_id], - fields=["_id", "name"] + folder_ids={selected_folder_id}, + fields={"id", "name"} )) product_name_by_id = {} product_ids = set() - for product_doc in product_docs: - product_id = product_doc["_id"] + for product_entity in product_entities: + product_id = product_entity["id"] product_ids.add(product_id) - product_name_by_id[product_id] = product_doc["name"] + product_name_by_id[product_id] = product_entity["name"] last_versions_by_product_id = self.find_last_versions(product_ids) @@ -1093,66 +1090,71 @@ class SwitchAssetDialog(QtWidgets.QDialog): for product_id, last_version in ( last_versions_by_product_id.items() ): - version_id = last_version["_id"] + version_id = last_version["id"] product_id_by_version_id[version_id] = product_id - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, version_ids=product_id_by_version_id.keys(), - fields=["name", "parent"] + fields={"name", "versionId"} ) repres_by_product_name = collections.defaultdict(set) - for repre_doc in repre_docs: - product_id = product_id_by_version_id[repre_doc["parent"]] + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + product_id = product_id_by_version_id[version_id] product_name = product_name_by_id[product_id] - repres_by_product_name[product_name].add(repre_doc["name"]) + repres_by_product_name[product_name].add(repre_entity["name"]) - for repre_doc in self._repre_docs_by_id.values(): - version_doc = self._version_docs_by_id[repre_doc["parent"]] - product_doc = self._product_docs_by_id[version_doc["parent"]] - repre_names = repres_by_product_name[product_doc["name"]] - if repre_doc["name"] not in repre_names: + for repre_entity in self._repre_entities_by_id.values(): + version_id = repre_entity["versionId"] + version_entity = self._version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = self._product_entities_by_id[product_id] + repre_names = repres_by_product_name[product_entity["name"]] + if repre_entity["name"] not in repre_names: validation_state.repre_ok = False break return # [ ] [x] [ ] - # Product documents - product_docs = get_subsets( + # Product entities + product_entities = ayon_api.get_products( project_name, - asset_ids=self._folder_docs_by_id.keys(), - subset_names=[selected_product_name], - fields=["_id", "name", "parent"] + folder_ids=self._folder_entities_by_id.keys(), + product_names={selected_product_name}, + fields={"id", "name", "folderId"} ) - product_docs_by_id = {} - for product_doc in product_docs: - product_docs_by_id[product_doc["_id"]] = product_doc + product_entities_by_id = {} + for product_entity in product_entities: + product_entities_by_id[product_entity["id"]] = product_entity last_versions_by_product_id = self.find_last_versions( - product_docs_by_id.keys() + product_entities_by_id.keys() ) product_id_by_version_id = {} for product_id, last_version in last_versions_by_product_id.items(): - version_id = last_version["_id"] + version_id = last_version["id"] product_id_by_version_id[version_id] = product_id - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, version_ids=product_id_by_version_id.keys(), - fields=["name", "parent"] + fields={"name", "versionId"} ) repres_by_folder_id = collections.defaultdict(set) - for repre_doc in repre_docs: - product_id = product_id_by_version_id[repre_doc["parent"]] - folder_id = product_docs_by_id[product_id]["parent"] - repres_by_folder_id[folder_id].add(repre_doc["name"]) + for repre_entity in repre_entities: + product_id = product_id_by_version_id[repre_entity["versionId"]] + folder_id = product_entities_by_id[product_id]["folderId"] + repres_by_folder_id[folder_id].add(repre_entity["name"]) - for repre_doc in self._repre_docs_by_id.values(): - version_doc = self._version_docs_by_id[repre_doc["parent"]] - product_doc = self._product_docs_by_id[version_doc["parent"]] - folder_id = product_doc["parent"] + for repre_entity in self._repre_entities_by_id.values(): + version_id = repre_entity["versionId"] + version_entity = self._version_entities_by_id[version_id] + product_id = version_entity["productId"] + product_entity = self._product_entities_by_id[product_id] + folder_id = product_entity["folderId"] repre_names = repres_by_folder_id[folder_id] - if repre_doc["name"] not in repre_names: + if repre_entity["name"] not in repre_names: validation_state.repre_ok = False break @@ -1182,57 +1184,59 @@ class SwitchAssetDialog(QtWidgets.QDialog): if selected_folder_id: folder_ids = {selected_folder_id} else: - folder_ids = set(self._folder_docs_by_id.keys()) + folder_ids = set(self._folder_entities_by_id.keys()) product_names = None if selected_product_name: product_names = [selected_product_name] - product_docs = list(get_subsets( + product_entities = list(ayon_api.get_products( project_name, - subset_names=product_names, - asset_ids=folder_ids + product_names=product_names, + folder_ids=folder_ids )) product_ids = set() - product_docs_by_parent_and_name = collections.defaultdict(dict) - for product_doc in product_docs: - product_ids.add(product_doc["_id"]) - folder_id = product_doc["parent"] - name = product_doc["name"] - product_docs_by_parent_and_name[folder_id][name] = product_doc + product_entities_by_parent_and_name = collections.defaultdict(dict) + for product_entity in product_entities: + product_ids.add(product_entity["id"]) + folder_id = product_entity["folderId"] + name = product_entity["name"] + product_entities_by_parent_and_name[folder_id][name] = ( + product_entity + ) # versions - _version_docs = get_versions(project_name, subset_ids=product_ids) - version_docs = list(reversed( - sorted(_version_docs, key=lambda item: item["name"]) - )) - - hero_version_docs = list(get_hero_versions( - project_name, subset_ids=product_ids + _version_entities = ayon_api.get_versions( + project_name, product_ids=product_ids + ) + version_entities = list(reversed( + sorted(_version_entities, key=lambda item: item["version"]) )) version_ids = set() - version_docs_by_parent_id_and_name = collections.defaultdict(dict) - for version_doc in version_docs: - version_ids.add(version_doc["_id"]) - product_id = version_doc["parent"] - name = version_doc["name"] - version_docs_by_parent_id_and_name[product_id][name] = version_doc + version_entities_by_product_id = collections.defaultdict(dict) + hero_version_entities_by_product_id = {} + for version_entity in version_entities: + version_ids.add(version_entity["id"]) + product_id = version_entity["productId"] + version = version_entity["version"] + if version < 0: + hero_version_entities_by_product_id[product_id] = ( + version_entity + ) + continue + version_entities_by_product_id[product_id][version] = ( + version_entity + ) - hero_version_docs_by_parent_id = {} - for hero_version_doc in hero_version_docs: - version_ids.add(hero_version_doc["_id"]) - parent_id = hero_version_doc["parent"] - hero_version_docs_by_parent_id[parent_id] = hero_version_doc - - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, version_ids=version_ids ) - repre_docs_by_parent_id_by_name = collections.defaultdict(dict) - for repre_doc in repre_docs: - parent_id = repre_doc["parent"] - name = repre_doc["name"] - repre_docs_by_parent_id_by_name[parent_id][name] = repre_doc + repre_entities_by_name_version_id = collections.defaultdict(dict) + for repre_entity in repre_entities: + version_id = repre_entity["versionId"] + name = repre_entity["name"] + repre_entities_by_name_version_id[version_id][name] = repre_entity for container in self._items: self._switch_container( @@ -1241,10 +1245,10 @@ class SwitchAssetDialog(QtWidgets.QDialog): selected_folder_id, selected_product_name, selected_representation, - product_docs_by_parent_and_name, - version_docs_by_parent_id_and_name, - hero_version_docs_by_parent_id, - repre_docs_by_parent_id_by_name, + product_entities_by_parent_and_name, + version_entities_by_product_id, + hero_version_entities_by_product_id, + repre_entities_by_name_version_id, ) self.switched.emit() @@ -1258,81 +1262,81 @@ class SwitchAssetDialog(QtWidgets.QDialog): selected_folder_id, selected_product_name, selected_representation, - product_docs_by_parent_and_name, - version_docs_by_parent_id_and_name, - hero_version_docs_by_parent_id, - repre_docs_by_parent_id_by_name, + product_entities_by_parent_and_name, + version_entities_by_product_id, + hero_version_entities_by_product_id, + repre_entities_by_name_version_id, ): container_repre_id = container["representation"] - container_repre = self._repre_docs_by_id[container_repre_id] + container_repre = self._repre_entities_by_id[container_repre_id] container_repre_name = container_repre["name"] - container_version_id = container_repre["parent"] + container_version_id = container_repre["versionId"] - container_version = self._version_docs_by_id[container_version_id] + container_version = self._version_entities_by_id[container_version_id] - container_product_id = container_version["parent"] - container_product = self._product_docs_by_id[container_product_id] + container_product_id = container_version["productId"] + container_product = self._product_entities_by_id[container_product_id] container_product_name = container_product["name"] - container_folder_id = container_product["parent"] + container_folder_id = container_product["folderId"] if selected_folder_id: folder_id = selected_folder_id else: folder_id = container_folder_id - products_by_name = product_docs_by_parent_and_name[folder_id] + products_by_name = product_entities_by_parent_and_name[folder_id] if selected_product_name: - product_doc = products_by_name[selected_product_name] + product_entity = products_by_name[selected_product_name] else: - product_doc = products_by_name[container_product["name"]] + product_entity = products_by_name[container_product["name"]] - repre_doc = None - product_id = product_doc["_id"] - if container_version["type"] == "hero_version": - hero_version = hero_version_docs_by_parent_id.get( + repre_entity = None + product_id = product_entity["id"] + if container_version["version"] < 0: + hero_version = hero_version_entities_by_product_id.get( product_id ) if hero_version: - _repres = repre_docs_by_parent_id_by_name.get( - hero_version["_id"] + _repres = repre_entities_by_name_version_id.get( + hero_version["id"] ) if selected_representation: - repre_doc = _repres.get(selected_representation) + repre_entity = _repres.get(selected_representation) else: - repre_doc = _repres.get(container_repre_name) + repre_entity = _repres.get(container_repre_name) - if not repre_doc: - version_docs_by_name = ( - version_docs_by_parent_id_and_name[product_id] + if not repre_entity: + version_entities_by_version = ( + version_entities_by_product_id[product_id] ) - # If asset or subset are selected for switching, we use latest + # If folder or product are selected for switching, we use latest # version else we try to keep the current container version. - version_name = None + version = None if ( selected_folder_id in (None, container_folder_id) and selected_product_name in (None, container_product_name) ): - version_name = container_version.get("name") + version = container_version.get("version") - version_doc = None - if version_name is not None: - version_doc = version_docs_by_name.get(version_name) + version_entity = None + if version is not None: + version_entity = version_entities_by_version.get(version) - if version_doc is None: - version_name = max(version_docs_by_name) - version_doc = version_docs_by_name[version_name] + if version_entity is None: + version_name = max(version_entities_by_version) + version_entity = version_entities_by_version[version_name] - version_id = version_doc["_id"] - repres_by_name = repre_docs_by_parent_id_by_name[version_id] + version_id = version_entity["id"] + repres_by_name = repre_entities_by_name_version_id[version_id] if selected_representation: - repre_doc = repres_by_name[selected_representation] + repre_entity = repres_by_name[selected_representation] else: - repre_doc = repres_by_name[container_repre_name] + repre_entity = repres_by_name[container_repre_name] error = None try: - switch_container(container, repre_doc, loader) + switch_container(container, repre_entity, loader) except ( LoaderSwitchNotImplementedError, IncompatibleLoaderError, diff --git a/client/ayon_core/tools/sceneinventory/view.py b/client/ayon_core/tools/sceneinventory/view.py index 80c89338f5..d576bdc139 100644 --- a/client/ayon_core/tools/sceneinventory/view.py +++ b/client/ayon_core/tools/sceneinventory/view.py @@ -4,16 +4,10 @@ import logging import itertools from functools import partial +import ayon_api from qtpy import QtWidgets, QtCore import qtawesome -from ayon_core.client import ( - get_version_by_id, - get_versions, - get_hero_versions, - get_representation_by_id, - get_representations, -) from ayon_core import style from ayon_core.pipeline import ( HeroVersionType, @@ -97,50 +91,54 @@ class SceneInventoryView(QtWidgets.QTreeView): pass project_name = self._controller.get_current_project_name() - repre_docs = get_representations( - project_name, representation_ids=repre_ids, fields=["parent"] + repre_entities = ayon_api.get_representations( + project_name, + representation_ids=repre_ids, + fields={"versionId"} ) version_ids = { - repre_doc["parent"] - for repre_doc in repre_docs + repre_entity["versionId"] + for repre_entity in repre_entities } - loaded_versions = get_versions( - project_name, version_ids=version_ids, hero=True + loaded_versions = ayon_api.get_versions( + project_name, version_ids=version_ids ) loaded_hero_versions = [] - versions_by_parent_id = collections.defaultdict(list) + versions_by_product_id = collections.defaultdict(list) product_ids = set() - for version in loaded_versions: - if version["type"] == "hero_version": - loaded_hero_versions.append(version) + for version_entity in loaded_versions: + version = version_entity["version"] + if version < 0: + loaded_hero_versions.append(version_entity) else: - parent_id = version["parent"] - versions_by_parent_id[parent_id].append(version) - product_ids.add(parent_id) + product_id = version_entity["productId"] + versions_by_product_id[product_id].append(version_entity) + product_ids.add(product_id) - all_versions = get_versions( - project_name, subset_ids=product_ids, hero=True + all_versions = ayon_api.get_versions( + project_name, product_ids=product_ids ) hero_versions = [] - versions = [] - for version in all_versions: - if version["type"] == "hero_version": - hero_versions.append(version) + version_entities = [] + for version_entity in all_versions: + version = version_entity["version"] + if version < 0: + hero_versions.append(version_entity) else: - versions.append(version) + version_entities.append(version_entity) has_loaded_hero_versions = len(loaded_hero_versions) > 0 has_available_hero_version = len(hero_versions) > 0 has_outdated = False - for version in versions: - parent_id = version["parent"] - current_versions = versions_by_parent_id[parent_id] + for version_entity in version_entities: + product_id = version_entity["productId"] + current_versions = versions_by_product_id[product_id] for current_version in current_versions: - if current_version["name"] < version["name"]: + if current_version["version"] < version_entity["version"]: has_outdated = True break @@ -155,46 +153,52 @@ class SceneInventoryView(QtWidgets.QTreeView): for item in items } - repre_docs = get_representations( + repre_entities = ayon_api.get_representations( project_name, representation_ids=repre_ids, - fields=["parent"] + fields={"id", "versionId"} ) - version_ids = set() version_id_by_repre_id = {} - for repre_doc in repre_docs: - version_id = repre_doc["parent"] - repre_id = str(repre_doc["_id"]) + for repre_entity in repre_entities: + repre_id = repre_entity["id"] + version_id = repre_entity["versionId"] version_id_by_repre_id[repre_id] = version_id - version_ids.add(version_id) + version_ids = set(version_id_by_repre_id.values()) - hero_versions = get_hero_versions( + src_version_entity_by_id = { + version_entity["id"]: version_entity + for version_entity in ayon_api.get_versions( + project_name, + version_ids, + fields={"productId", "version"} + ) + } + hero_versions_by_product_id = {} + for version_entity in src_version_entity_by_id.values(): + version = version_entity["version"] + if version < 0: + product_id = version_entity["productId"] + hero_versions_by_product_id[product_id] = abs(version) + + if not hero_versions_by_product_id: + return + + standard_versions = ayon_api.get_versions( project_name, - version_ids=version_ids, - fields=["version_id"] + product_ids=hero_versions_by_product_id.keys(), + versions=hero_versions_by_product_id.values() ) - - hero_src_version_ids = set() - for hero_version in hero_versions: - version_id = hero_version["version_id"] - hero_src_version_ids.add(version_id) - hero_version_id = hero_version["_id"] - for _repre_id, current_version_id in ( - version_id_by_repre_id.items() - ): - if current_version_id == hero_version_id: - version_id_by_repre_id[_repre_id] = version_id - - version_docs = get_versions( - project_name, - version_ids=hero_src_version_ids, - fields=["name"] - ) - version_name_by_id = {} - for version_doc in version_docs: - version_name_by_id[version_doc["_id"]] = \ - version_doc["name"] + standard_version_by_product_id = { + product_id: {} + for product_id in hero_versions_by_product_id.keys() + } + for version_entity in standard_versions: + product_id = version_entity["productId"] + version = version_entity["version"] + standard_version_by_product_id[product_id][version] = ( + version_entity + ) # Specify version per item to update to update_items = [] @@ -202,10 +206,20 @@ class SceneInventoryView(QtWidgets.QTreeView): for item in items: repre_id = item["representation"] version_id = version_id_by_repre_id.get(repre_id) - version_name = version_name_by_id.get(version_id) - if version_name is not None: + version_entity = src_version_entity_by_id.get(version_id) + if not version_entity or version_entity["version"] >= 0: + continue + product_id = version_entity["productId"] + version_entities_by_version = ( + standard_version_by_product_id[product_id] + ) + new_version = hero_versions_by_product_id.get(product_id) + new_version_entity = version_entities_by_version.get( + new_version + ) + if new_version_entity is not None: update_items.append(item) - update_versions.append(version_name) + update_versions.append(new_version) self._update_containers(update_items, update_versions) update_icon = qtawesome.icon( @@ -249,8 +263,9 @@ class SceneInventoryView(QtWidgets.QTreeView): menu ) change_to_hero.triggered.connect( - lambda: self._update_containers(items, - version=HeroVersionType(-1)) + lambda: self._update_containers( + items, version=HeroVersionType(-1) + ) ) # set version @@ -608,44 +623,31 @@ class SceneInventoryView(QtWidgets.QTreeView): project_name = self._controller.get_current_project_name() # Get available versions for active representation - repre_doc = get_representation_by_id( + repre_entity = ayon_api.get_representation_by_id( project_name, active["representation"], - fields=["parent"] + fields={"versionId"} ) - repre_version_doc = get_version_by_id( + repre_version_entity = ayon_api.get_version_by_id( project_name, - repre_doc["parent"], - fields=["parent"] + repre_entity["versionId"], + fields={"productId"} ) - version_docs = list(get_versions( + version_entities = list(ayon_api.get_versions( project_name, - subset_ids=[repre_version_doc["parent"]], - hero=True + product_ids={repre_version_entity["productId"]}, )) hero_version = None standard_versions = [] - for version_doc in version_docs: - if version_doc["type"] == "hero_version": - hero_version = version_doc + for version_entity in version_entities: + if version_entity["version"] < 0: + hero_version = version_entity else: - standard_versions.append(version_doc) - versions = list(reversed( - sorted(standard_versions, key=lambda item: item["name"]) - )) - if hero_version: - _version_id = hero_version["version_id"] - for _version in versions: - if _version["_id"] != _version_id: - continue - - hero_version["name"] = HeroVersionType( - _version["name"] - ) - hero_version["data"] = _version["data"] - break + standard_versions.append(version_entity) + standard_versions.sort(key=lambda item: item["version"]) + standard_versions.reverse() # Get index among the listed versions current_item = None @@ -653,15 +655,15 @@ class SceneInventoryView(QtWidgets.QTreeView): if isinstance(current_version, HeroVersionType): current_item = hero_version else: - for version in versions: - if version["name"] == current_version: - current_item = version + for version_entity in standard_versions: + if version_entity["version"] == current_version: + current_item = version_entity break all_versions = [] if hero_version: all_versions.append(hero_version) - all_versions.extend(versions) + all_versions.extend(standard_versions) if current_item: index = all_versions.index(current_item) @@ -670,11 +672,10 @@ class SceneInventoryView(QtWidgets.QTreeView): versions_by_label = dict() labels = [] - for version in all_versions: - is_hero = version["type"] == "hero_version" - label = format_version(version["name"], is_hero) + for version_entity in all_versions: + label = format_version(version_entity["version"]) labels.append(label) - versions_by_label[label] = version["name"] + versions_by_label[label] = version_entity["version"] label, state = QtWidgets.QInputDialog.getItem( self, @@ -689,6 +690,8 @@ class SceneInventoryView(QtWidgets.QTreeView): if label: version = versions_by_label[label] + if version < 0: + version = HeroVersionType(version) self._update_containers(items, version) def _show_switch_dialog(self, items): diff --git a/client/ayon_core/tools/texture_copy/app.py b/client/ayon_core/tools/texture_copy/app.py index 120051060b..b3484fbcd0 100644 --- a/client/ayon_core/tools/texture_copy/app.py +++ b/client/ayon_core/tools/texture_copy/app.py @@ -1,12 +1,13 @@ import os import re + import click - import speedcopy +import ayon_api -from ayon_core.client import get_project, get_asset_by_name from ayon_core.lib import Terminal from ayon_core.pipeline import Anatomy +from ayon_core.pipeline.template_data import get_template_data t = Terminal() @@ -24,33 +25,21 @@ class TextureCopy: if os.path.splitext(x)[1].lower() in texture_extensions) return textures - def _get_destination_path(self, asset, project): - project_name = project["name"] - hierarchy = "" - parents = asset['data']['parents'] - if parents and len(parents) > 0: - hierarchy = os.path.join(*parents) + def _get_destination_path(self, folder_entity, project_entity): + project_name = project_entity["name"] product_name = "Main" product_type = "texture" - template_data = { - "project": { - "name": project_name, - "code": project['data']['code'] - }, - "asset": asset["name"], + template_data = get_template_data(project_entity, folder_entity) + template_data.update({ "family": product_type, "subset": product_name, - "folder": { - "name": asset["name"], - }, "product": { "name": product_name, "type": product_type, }, - "hierarchy": hierarchy - } - anatomy = Anatomy(project_name) + }) + anatomy = Anatomy(project_name, project_entity=project_entity) template_obj = anatomy.templates_obj["texture"]["path"] return template_obj.format_strict(template_data) @@ -78,9 +67,9 @@ class TextureCopy: t.echo("!!! {}".format(e)) exit(1) - def process(self, asset_name, project_name, path): + def process(self, project_name, folder_path, path): """ - Process all textures found in path and copy them to asset under + Process all textures found in path and copy them to folder under project. """ @@ -92,20 +81,24 @@ class TextureCopy: else: t.echo(">>> Found {} textures ...".format(len(textures))) - project = get_project(project_name) - if not project: + project_entity = ayon_api.get_project(project_name) + if not project_entity: t.echo("!!! Project name [ {} ] not found.".format(project_name)) exit(1) - asset = get_asset_by_name(project_name, asset_name) - if not asset: - t.echo("!!! Asset [ {} ] not found in project".format(asset_name)) + folder_entity = ayon_api.get_folder_by_path(project_name, folder_path) + if not folder_entity: + t.echo( + "!!! Folder [ {} ] not found in project".format(folder_path) + ) exit(1) - t.echo((">>> Project [ {} ] and " - "asset [ {} ] seems to be OK ...").format(project['name'], - asset['name'])) + t.echo( + ( + ">>> Project [ {} ] and folder [ {} ] seems to be OK ..." + ).format(project_entity['name'], folder_entity['path']) + ) - dst_path = self._get_destination_path(asset, project) + dst_path = self._get_destination_path(folder_entity, project_entity) t.echo("--- Using [ {} ] as destination path".format(dst_path)) if not os.path.exists(dst_path): try: @@ -135,15 +128,15 @@ class TextureCopy: @click.command() -@click.option('--asset', required=True) @click.option('--project', required=True) +@click.option('--folder', required=True) @click.option('--path', required=True) -def texture_copy(asset, project, path): +def texture_copy(project, folder, path): t.echo("*** Running Texture tool ***") t.echo(">>> Initializing avalon session ...") os.environ["AYON_PROJECT_NAME"] = project - os.environ["AYON_FOLDER_PATH"] = asset - TextureCopy().process(asset, project, path) + os.environ["AYON_FOLDER_PATH"] = folder + TextureCopy().process(project, folder, path) if __name__ == '__main__': diff --git a/client/ayon_core/tools/traypublisher/window.py b/client/ayon_core/tools/traypublisher/window.py index 210e77f0fa..988c22819a 100644 --- a/client/ayon_core/tools/traypublisher/window.py +++ b/client/ayon_core/tools/traypublisher/window.py @@ -41,7 +41,6 @@ class TrayPublisherController(QtPublisherController): def reset_hierarchy_cache(self): self._hierarchy_model.reset() - self._asset_docs_cache.reset() def get_project_items(self, sender=None): return self._projects_model.get_project_items(sender) diff --git a/client/ayon_core/tools/utils/assets_widget.py b/client/ayon_core/tools/utils/assets_widget.py deleted file mode 100644 index 7c3fd8d97c..0000000000 --- a/client/ayon_core/tools/utils/assets_widget.py +++ /dev/null @@ -1,696 +0,0 @@ -import time -import collections - -from qtpy import QtWidgets, QtCore, QtGui -import qtawesome - -from ayon_core.client import ( - get_project, - get_assets, -) -from ayon_core.style import ( - get_default_tools_icon_color, - get_default_entity_icon_color, -) -from ayon_core.tools.flickcharm import FlickCharm - -from .views import ( - TreeViewSpinner, - DeselectableTreeView -) -from .widgets import PlaceholderLineEdit -from .models import RecursiveSortFilterProxyModel -from .lib import ( - DynamicQThread, - get_qta_icon_by_name_and_color -) - -ASSET_ID_ROLE = QtCore.Qt.UserRole + 1 -ASSET_NAME_ROLE = QtCore.Qt.UserRole + 2 -ASSET_LABEL_ROLE = QtCore.Qt.UserRole + 3 -ASSET_UNDERLINE_COLORS_ROLE = QtCore.Qt.UserRole + 4 -ASSET_PATH_ROLE = QtCore.Qt.UserRole + 5 - - -def _get_default_asset_icon_name(has_children): - if has_children: - return "fa.folder" - return "fa.folder-o" - - -def _get_asset_icon_color_from_doc(asset_doc): - if asset_doc: - return asset_doc["data"].get("color") - return None - - -def _get_asset_icon_name_from_doc(asset_doc): - if asset_doc: - return asset_doc["data"].get("icon") - return None - - -def _get_asset_icon_color(asset_doc): - icon_color = _get_asset_icon_color_from_doc(asset_doc) - if icon_color: - return icon_color - return get_default_entity_icon_color() - - -def _get_asset_icon_name(asset_doc, has_children=True): - icon_name = _get_asset_icon_name_from_doc(asset_doc) - if icon_name: - return icon_name - return _get_default_asset_icon_name(has_children) - - -def get_asset_icon(asset_doc, has_children=False): - """Get asset icon. - - Deprecated: - This function will be removed in future releases. Use on your own - risk. - - Args: - asset_doc (dict): Asset document. - has_children (Optional[bool]): Asset has children assets. - - Returns: - QIcon: Asset icon. - - """ - icon_name = _get_asset_icon_name(asset_doc, has_children) - icon_color = _get_asset_icon_color(asset_doc) - - return get_qta_icon_by_name_and_color(icon_name, icon_color) - - -class _AssetsView(TreeViewSpinner, DeselectableTreeView): - """Asset items view. - - Adds abilities to deselect, show loading spinner and add flick charm - (scroll by mouse/touchpad click and move). - """ - - def __init__(self, parent=None): - super(_AssetsView, self).__init__(parent) - self.setIndentation(15) - self.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.setHeaderHidden(True) - - self._flick_charm_activated = False - self._flick_charm = FlickCharm(parent=self) - self._before_flick_scroll_mode = None - - def activate_flick_charm(self): - if self._flick_charm_activated: - return - self._flick_charm_activated = True - self._before_flick_scroll_mode = self.verticalScrollMode() - self._flick_charm.activateOn(self) - self.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel) - - def deactivate_flick_charm(self): - if not self._flick_charm_activated: - return - self._flick_charm_activated = False - self._flick_charm.deactivateFrom(self) - if self._before_flick_scroll_mode is not None: - self.setVerticalScrollMode(self._before_flick_scroll_mode) - - def mousePressEvent(self, event): - index = self.indexAt(event.pos()) - if not index.isValid(): - modifiers = QtWidgets.QApplication.keyboardModifiers() - if modifiers == QtCore.Qt.ShiftModifier: - return - elif modifiers == QtCore.Qt.ControlModifier: - return - - super(_AssetsView, self).mousePressEvent(event) - - def set_loading_state(self, loading, empty): - """Change loading state. - - TODO: Separate into 2 individual methods. - - Args: - loading(bool): Is loading. - empty(bool): Is model empty. - """ - if self.is_loading != loading: - if loading: - self.spinner.repaintNeeded.connect( - self.viewport().update - ) - else: - self.spinner.repaintNeeded.disconnect() - self.viewport().update() - - self.is_loading = loading - self.is_empty = empty - - -class _AssetModel(QtGui.QStandardItemModel): - """A model listing assets in the active project. - - The assets are displayed in a treeview, they are visually parented by - a `visualParent` field in the database containing an `_id` to a parent - asset. - - Asset document may have defined label, icon or icon color. - - Loading of data for model happens in thread which means that refresh - is not sequential. When refresh is triggered it is required to listen for - 'refreshed' signal. - - Args: - parent (QObject): Parent Qt object. - """ - - _doc_fetched = QtCore.Signal() - refreshed = QtCore.Signal(bool) - - # Asset document projection - _asset_projection = { - "name": 1, - "parent": 1, - "data.visualParent": 1, - "data.label": 1, - "data.icon": 1, - "data.color": 1 - } - - def __init__(self, parent=None): - super(_AssetModel, self).__init__(parent=parent) - - self._refreshing = False - self._doc_fetching_thread = None - self._doc_fetching_stop = False - self._doc_payload = [] - - self._doc_fetched.connect(self._on_docs_fetched) - - self._item_ids_with_color = set() - self._items_by_asset_id = {} - - self._project_name = None - self._last_project_name = None - - @property - def refreshing(self): - return self._refreshing - - def get_index_by_asset_id(self, asset_id): - item = self._items_by_asset_id.get(asset_id) - if item is not None: - return item.index() - return QtCore.QModelIndex() - - def get_indexes_by_asset_ids(self, asset_ids): - return [ - self.get_index_by_asset_id(asset_id) - for asset_id in asset_ids - ] - - def get_index_by_asset_name(self, asset_name): - indexes = self.get_indexes_by_asset_names([asset_name]) - for index in indexes: - if index.isValid(): - return index - return indexes[0] - - def get_indexes_by_asset_names(self, asset_names): - asset_ids_by_name = { - asset_name: None - for asset_name in asset_names - } - - for asset_id, item in self._items_by_asset_id.items(): - asset_name = item.data(ASSET_NAME_ROLE) - if asset_name in asset_ids_by_name: - asset_ids_by_name[asset_name] = asset_id - - asset_ids = [ - asset_ids_by_name[asset_name] - for asset_name in asset_names - ] - - return self.get_indexes_by_asset_ids(asset_ids) - - def get_project_name(self): - return self._project_name - - def set_project_name(self, project_name, refresh): - if self._project_name == project_name: - return - self._project_name = project_name - if refresh: - self.refresh() - - def refresh(self, force=False): - """Refresh the data for the model. - - Args: - force (bool): Stop currently running refresh start new refresh. - """ - # Skip fetch if there is already other thread fetching documents - if self._refreshing: - if not force: - return - self.stop_refresh() - - project_name = self._project_name - clear_model = False - if project_name != self._last_project_name: - clear_model = True - self._last_project_name = project_name - - if clear_model: - self._clear_items() - - # Fetch documents from mongo - # Restart payload - self._refreshing = True - self._doc_payload = [] - self._doc_fetching_thread = DynamicQThread(self._threaded_fetch) - self._doc_fetching_thread.start() - - def stop_refresh(self): - self._stop_fetch_thread() - - def _clear_items(self): - root_item = self.invisibleRootItem() - root_item.removeRows(0, root_item.rowCount()) - self._items_by_asset_id = {} - self._item_ids_with_color = set() - - def _on_docs_fetched(self): - # Make sure refreshing did not change - # - since this line is refreshing sequential and - # triggering of new refresh will happen when this method is done - if not self._refreshing: - self._clear_items() - return - - self._fill_assets(self._doc_payload) - - self.refreshed.emit(bool(self._items_by_asset_id)) - - self._stop_fetch_thread() - - def _fill_assets(self, asset_docs): - # Collect asset documents as needed - asset_ids = set() - asset_docs_by_id = {} - asset_ids_by_parents = collections.defaultdict(set) - for asset_doc in asset_docs: - asset_id = asset_doc["_id"] - asset_data = asset_doc.get("data") or {} - parent_id = asset_data.get("visualParent") - asset_ids.add(asset_id) - asset_docs_by_id[asset_id] = asset_doc - asset_ids_by_parents[parent_id].add(asset_id) - - # Prepare removed asset ids - removed_asset_ids = ( - set(self._items_by_asset_id.keys()) - set(asset_docs_by_id.keys()) - ) - - # Prepare queue for adding new items - asset_items_queue = collections.deque() - - # Queue starts with root item and 'visualParent' None - root_item = self.invisibleRootItem() - asset_items_queue.append((None, root_item)) - - while asset_items_queue: - # Get item from queue - parent_id, parent_item = asset_items_queue.popleft() - # Skip if there are no children - children_ids = asset_ids_by_parents[parent_id] - - # Go through current children of parent item - # - find out items that were deleted and skip creation of already - # existing items - for row in reversed(range(parent_item.rowCount())): - child_item = parent_item.child(row, 0) - asset_id = child_item.data(ASSET_ID_ROLE) - # Remove item that is not available - if asset_id not in children_ids: - if asset_id in removed_asset_ids: - # Remove and destroy row - parent_item.removeRow(row) - else: - # Just take the row from parent without destroying - parent_item.takeRow(row) - continue - - # Remove asset id from `children_ids` set - # - is used as set for creation of "new items" - children_ids.remove(asset_id) - # Add existing children to queue - asset_items_queue.append((asset_id, child_item)) - - new_items = [] - for asset_id in children_ids: - # Look for item in cache (maybe parent changed) - item = self._items_by_asset_id.get(asset_id) - # Create new item if was not found - if item is None: - item = QtGui.QStandardItem() - item.setEditable(False) - item.setData(asset_id, ASSET_ID_ROLE) - self._items_by_asset_id[asset_id] = item - new_items.append(item) - # Add item to queue - asset_items_queue.append((asset_id, item)) - - if new_items: - parent_item.appendRows(new_items) - - # Remove cache of removed items - for asset_id in removed_asset_ids: - self._items_by_asset_id.pop(asset_id) - - # Refresh data - # - all items refresh all data except id - for asset_id, item in self._items_by_asset_id.items(): - asset_doc = asset_docs_by_id[asset_id] - - asset_name = asset_doc["name"] - if item.data(ASSET_NAME_ROLE) != asset_name: - item.setData(asset_name, ASSET_NAME_ROLE) - - asset_data = asset_doc.get("data") or {} - asset_label = asset_data.get("label") or asset_name - if item.data(ASSET_LABEL_ROLE) != asset_label: - item.setData(asset_label, QtCore.Qt.DisplayRole) - item.setData(asset_label, ASSET_LABEL_ROLE) - - has_children = item.rowCount() > 0 - icon = get_asset_icon(asset_doc, has_children) - item.setData(icon, QtCore.Qt.DecorationRole) - - def _threaded_fetch(self): - asset_docs = self._fetch_asset_docs() - if not self._refreshing: - return - - self._doc_payload = asset_docs - - # Emit doc fetched only if was not stopped - self._doc_fetched.emit() - - def _fetch_asset_docs(self): - project_name = self.get_project_name() - if not project_name: - return [] - - project_doc = get_project(project_name, fields=["_id"]) - if not project_doc: - return [] - - # Get all assets sorted by name - return list( - get_assets(project_name, fields=self._asset_projection.keys()) - ) - - def _stop_fetch_thread(self): - self._refreshing = False - if self._doc_fetching_thread is not None: - while self._doc_fetching_thread.isRunning(): - time.sleep(0.01) - self._doc_fetching_thread = None - - -class _AssetsWidget(QtWidgets.QWidget): - """Base widget to display a tree of assets with filter. - - Assets have only one column and are sorted by name. - - Refreshing of assets happens in thread so calling 'refresh' method - is not sequential. To capture moment when refreshing is finished listen - to 'refreshed' signal. - - To capture selection changes listen to 'selection_changed' signal. It won't - send any information about new selection as it may be different based on - inheritance changes. - - Args: - parent (QWidget): Parent Qt widget. - """ - - # on model refresh - refresh_triggered = QtCore.Signal() - refreshed = QtCore.Signal() - # on view selection change - selection_changed = QtCore.Signal() - # It was double clicked on view - double_clicked = QtCore.Signal() - - def __init__(self, parent=None): - super(_AssetsWidget, self).__init__(parent=parent) - - # Tree View - model = self._create_source_model() - proxy = self._create_proxy_model(model) - - view = _AssetsView(self) - view.setModel(proxy) - - header_widget = QtWidgets.QWidget(self) - - current_asset_icon = qtawesome.icon( - "fa.arrow-down", color=get_default_tools_icon_color() - ) - current_asset_btn = QtWidgets.QPushButton(header_widget) - current_asset_btn.setIcon(current_asset_icon) - current_asset_btn.setToolTip("Go to Asset from current Session") - # Hide by default - current_asset_btn.setVisible(False) - - refresh_icon = qtawesome.icon( - "fa.refresh", color=get_default_tools_icon_color() - ) - refresh_btn = QtWidgets.QPushButton(header_widget) - refresh_btn.setIcon(refresh_icon) - refresh_btn.setToolTip("Refresh items") - - filter_input = PlaceholderLineEdit(header_widget) - filter_input.setPlaceholderText("Filter folders..") - - # Header - header_layout = QtWidgets.QHBoxLayout(header_widget) - header_layout.setContentsMargins(0, 0, 0, 0) - header_layout.addWidget(filter_input) - header_layout.addWidget(current_asset_btn) - header_layout.addWidget(refresh_btn) - - # Make header widgets expand vertically if there is a place - for widget in ( - current_asset_btn, - refresh_btn, - filter_input, - ): - size_policy = widget.sizePolicy() - size_policy.setVerticalPolicy( - QtWidgets.QSizePolicy.MinimumExpanding) - widget.setSizePolicy(size_policy) - - # Layout - layout = QtWidgets.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(header_widget, 0) - layout.addWidget(view, 1) - - # Signals/Slots - filter_input.textChanged.connect(self._on_filter_text_change) - - selection_model = view.selectionModel() - selection_model.selectionChanged.connect(self._on_selection_change) - refresh_btn.clicked.connect(self.refresh) - current_asset_btn.clicked.connect(self._on_current_asset_click) - view.doubleClicked.connect(self.double_clicked) - - self._header_widget = header_widget - self._filter_input = filter_input - self._refresh_btn = refresh_btn - self._current_asset_btn = current_asset_btn - self._model = model - self._proxy = proxy - self._view = view - - self._last_btns_height = None - - self._current_asset_name = None - - self.model_selection = {} - - @property - def header_widget(self): - return self._header_widget - - def get_project_name(self): - self._model.get_project_name() - - def set_project_name(self, project_name, refresh=True): - self._model.set_project_name(project_name, refresh) - - def set_current_asset_name(self, asset_name): - self._current_asset_name = asset_name - - def _create_source_model(self): - model = _AssetModel(parent=self) - model.refreshed.connect(self._on_model_refresh) - return model - - def _create_proxy_model(self, source_model): - proxy = RecursiveSortFilterProxyModel() - proxy.setSourceModel(source_model) - proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - proxy.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) - return proxy - - @property - def refreshing(self): - return self._model.refreshing - - def refresh(self): - self._refresh_model() - - def stop_refresh(self): - self._model.stop_refresh() - - def _get_current_asset_name(self): - return self._current_asset_name - - def _on_current_asset_click(self): - """Trigger change of asset to current context asset. - This separation gives ability to override this method and use it - in differnt way. - """ - - self.select_current_asset() - - def select_current_asset(self): - asset_name = self._get_current_asset_name() - if asset_name: - self.select_asset_by_name(asset_name) - - def set_refresh_btn_visibility(self, visible=None): - """Hide set refresh button. - Some tools may have their global refresh button or do not support - refresh at all. - """ - - if visible is None: - visible = not self._refresh_btn.isVisible() - self._refresh_btn.setVisible(visible) - - def set_current_asset_btn_visibility(self, visible=None): - """Hide set current asset button. - - Not all tools support using of current context asset. - """ - - if visible is None: - visible = not self._current_asset_btn.isVisible() - self._current_asset_btn.setVisible(visible) - - def select_asset(self, asset_id): - index = self._model.get_index_by_asset_id(asset_id) - new_index = self._proxy.mapFromSource(index) - self._select_indexes([new_index]) - - def select_asset_by_name(self, asset_name): - index = self._model.get_index_by_asset_name(asset_name) - new_index = self._proxy.mapFromSource(index) - self._select_indexes([new_index]) - - def activate_flick_charm(self): - self._view.activate_flick_charm() - - def deactivate_flick_charm(self): - self._view.deactivate_flick_charm() - - def _on_selection_change(self): - self.selection_changed.emit() - - def _on_filter_text_change(self, new_text): - self._proxy.setFilterFixedString(new_text) - - def _on_model_refresh(self, has_item): - """This method should be triggered on model refresh. - - Default implementation register this callback in '_create_source_model' - so if you're modifying model keep in mind that this method should be - called when refresh is done. - """ - - self._proxy.sort(0) - self._set_loading_state(loading=False, empty=not has_item) - self.refreshed.emit() - - def _refresh_model(self): - # Store selection - self._set_loading_state(loading=True, empty=True) - - # Trigger signal before refresh is called - self.refresh_triggered.emit() - # Refresh model - self._model.refresh() - - def _set_loading_state(self, loading, empty): - self._view.set_loading_state(loading, empty) - - def _clear_selection(self): - selection_model = self._view.selectionModel() - selection_model.clearSelection() - - def _select_indexes(self, indexes): - valid_indexes = [ - index - for index in indexes - if index.isValid() - ] - if not valid_indexes: - return - - selection_model = self._view.selectionModel() - selection_model.clearSelection() - - mode = ( - QtCore.QItemSelectionModel.Select - | QtCore.QItemSelectionModel.Rows - ) - for index in valid_indexes: - self._view.expand(self._proxy.parent(index)) - selection_model.select(index, mode) - self._view.setCurrentIndex(valid_indexes[0]) - - -class SingleSelectAssetsWidget(_AssetsWidget): - """Single selection asset widget. - - Contain single selection specific api methods. - - Deprecated: - This widget will be removed soon. Please do not use it in new code. - """ - - def get_selected_asset_id(self): - """Currently selected asset id.""" - selection_model = self._view.selectionModel() - indexes = selection_model.selectedRows() - for index in indexes: - return index.data(ASSET_ID_ROLE) - return None - - def get_selected_asset_name(self): - """Currently selected asset name.""" - selection_model = self._view.selectionModel() - indexes = selection_model.selectedRows() - for index in indexes: - return index.data(ASSET_NAME_ROLE) - return None diff --git a/client/ayon_core/tools/utils/host_tools.py b/client/ayon_core/tools/utils/host_tools.py index 8841a377cf..1eff746b9e 100644 --- a/client/ayon_core/tools/utils/host_tools.py +++ b/client/ayon_core/tools/utils/host_tools.py @@ -7,12 +7,9 @@ import os import pyblish.api -from ayon_core.host import IWorkfileHost, ILoadHost +from ayon_core.host import ILoadHost from ayon_core.lib import Logger -from ayon_core.pipeline import ( - registered_host, - get_current_asset_name, -) +from ayon_core.pipeline import registered_host from .lib import qt_app_context diff --git a/client/ayon_core/tools/utils/lib.py b/client/ayon_core/tools/utils/lib.py index e785cec390..741fc1f335 100644 --- a/client/ayon_core/tools/utils/lib.py +++ b/client/ayon_core/tools/utils/lib.py @@ -120,12 +120,12 @@ def paint_image_with_color(image, color): return pixmap -def format_version(value, hero_version=False): +def format_version(value): """Formats integer to displayable version name""" - label = "v{0:03d}".format(value) - if not hero_version: - return label - return "[{}]".format(label) + label = "v{0:03d}".format(abs(value)) + if value < 0: + return "[{}]".format(label) + return label @contextlib.contextmanager @@ -240,34 +240,6 @@ def get_default_task_icon(color=None): return get_qta_icon_by_name_and_color("fa.male", color) -def get_task_icon(project_doc, asset_doc, task_name): - """Get icon for a task. - - Icon should be defined by task type which is stored on project. - """ - - color = get_default_entity_icon_color() - - tasks_info = asset_doc.get("data", {}).get("tasks") or {} - task_info = tasks_info.get(task_name) or {} - task_icon = task_info.get("icon") - if task_icon: - icon = get_qta_icon_by_name_and_color(task_icon, color) - if icon is not None: - return icon - - task_type = task_info.get("type") - task_types = project_doc["config"]["tasks"] - - task_type_info = task_types.get(task_type) or {} - task_type_icon = task_type_info.get("icon") - if task_type_icon: - icon = get_qta_icon_by_name_and_color(task_icon, color) - if icon is not None: - return icon - return get_default_task_icon(color) - - def iter_model_rows(model, column, include_root=False): """Iterate over all row indices in a model""" indices = [QtCore.QModelIndex()] # start iteration at root diff --git a/client/ayon_core/tools/utils/models.py b/client/ayon_core/tools/utils/models.py index a4b6ad7885..92bed16e98 100644 --- a/client/ayon_core/tools/utils/models.py +++ b/client/ayon_core/tools/utils/models.py @@ -3,12 +3,6 @@ import logging import qtpy from qtpy import QtCore, QtGui -from ayon_core.client import get_projects -from .constants import ( - PROJECT_IS_ACTIVE_ROLE, - PROJECT_NAME_ROLE, - DEFAULT_PROJECT_LABEL -) log = logging.getLogger(__name__) diff --git a/client/ayon_core/tools/workfiles/control.py b/client/ayon_core/tools/workfiles/control.py index 6793cb23a9..3111c4d443 100644 --- a/client/ayon_core/tools/workfiles/control.py +++ b/client/ayon_core/tools/workfiles/control.py @@ -3,7 +3,6 @@ import shutil import ayon_api -from ayon_core.client import get_asset_by_id from ayon_core.host import IWorkfileHost from ayon_core.lib import Logger, emit_event from ayon_core.lib.events import QueuedEventSystem @@ -636,13 +635,10 @@ class BaseWorkfileController( folder = self.get_folder_entity(project_name, folder_id) if task is None: task = self.get_task_entity(project_name, task_id) - # NOTE keys should are OpenPype compatible return { "project_name": project_name, "folder_id": folder_id, "folder_path": folder["path"], - "asset_id": folder_id, - "asset_name": folder["name"], "task_id": task_id, "task_name": task["name"], "host_name": self.get_host_name(), @@ -663,15 +659,16 @@ class BaseWorkfileController( folder_id != self.get_current_folder_id() or task_name != self.get_current_task_name() ): - # Use OpenPype asset-like object - asset_doc = get_asset_by_id( + folder_entity = ayon_api.get_folder_by_id( event_data["project_name"], event_data["folder_id"], ) - change_current_context( - asset_doc, + task_entity = ayon_api.get_task_by_name( + event_data["project_name"], + event_data["folder_id"], event_data["task_name"] ) + change_current_context(folder_entity, task_entity) self._host_open_workfile(filepath) @@ -713,11 +710,15 @@ class BaseWorkfileController( folder_id != self.get_current_folder_id() or task_name != self.get_current_task_name() ): - # Use OpenPype asset-like object - asset_doc = get_asset_by_id(project_name, folder["id"]) + folder_entity = ayon_api.get_folder_by_id( + project_name, folder["id"] + ) + task_entity = ayon_api.get_task_by_name( + project_name, folder["id"], task_name + ) change_current_context( - asset_doc, - task["name"], + folder_entity, + task_entity, template_key=template_key ) diff --git a/client/ayon_core/tools/workfiles/models/workfiles.py b/client/ayon_core/tools/workfiles/models/workfiles.py index 1e9491b3d7..35a49a908f 100644 --- a/client/ayon_core/tools/workfiles/models/workfiles.py +++ b/client/ayon_core/tools/workfiles/models/workfiles.py @@ -6,12 +6,10 @@ import arrow import ayon_api from ayon_api.operations import OperationsSession -from ayon_core.client import get_project -from ayon_core.client.operations import ( - prepare_workfile_info_update_data, -) from ayon_core.pipeline.template_data import ( get_template_data, + get_task_template_data, + get_folder_template_data, ) from ayon_core.pipeline.workfile import ( get_workdir_with_workdir_data, @@ -26,42 +24,6 @@ from ayon_core.tools.workfiles.abstract import ( ) -def get_folder_template_data(folder): - if not folder: - return {} - parts = folder["path"].split("/") - parts.pop(-1) - hierarchy = "/".join(parts) - return { - "asset": folder["name"], - "folder": { - "name": folder["name"], - "type": folder["folderType"], - "path": folder["path"], - }, - "hierarchy": hierarchy, - } - - -def get_task_template_data(project_entity, task): - if not task: - return {} - short_name = None - task_type_name = task["taskType"] - for task_type_info in project_entity["taskTypes"]: - if task_type_info["name"] == task_type_name: - short_name = task_type_info["shortName"] - break - - return { - "task": { - "name": task["name"], - "type": task_type_name, - "short": short_name, - } - } - - class CommentMatcher(object): """Use anatomy and work file data to parse comments from filenames""" def __init__(self, extensions, file_template, data): @@ -140,7 +102,9 @@ class WorkareaModel: def _get_base_data(self): if self._base_data is None: - base_data = get_template_data(get_project(self.project_name)) + base_data = get_template_data( + ayon_api.get_project(self.project_name) + ) base_data["app"] = self._controller.get_host_name() self._base_data = base_data return copy.deepcopy(self._base_data) @@ -151,7 +115,7 @@ class WorkareaModel: folder = self._controller.get_folder_entity( self.project_name, folder_id ) - fill_data = get_folder_template_data(folder) + fill_data = get_folder_template_data(folder, self.project_name) self._fill_data_by_folder_id[folder_id] = fill_data return copy.deepcopy(fill_data) @@ -227,9 +191,9 @@ class WorkareaModel: task_type = fill_data.get("task", {}).get("type") # TODO cache return get_workfile_template_key( + self.project_name, task_type, self._controller.get_host_name(), - project_name=self.project_name ) def _get_last_workfile_version( @@ -507,22 +471,24 @@ class WorkfileEntitiesModel: if note is None: return + old_note = workfile_info.get("attrib", {}).get("note") + new_workfile_info = copy.deepcopy(workfile_info) attrib = new_workfile_info.setdefault("attrib", {}) attrib["description"] = note - update_data = prepare_workfile_info_update_data( - workfile_info, new_workfile_info - ) self._cache[identifier] = new_workfile_info self._items.pop(identifier, None) - if not update_data: + if old_note == note: return project_name = self._controller.get_current_project_name() session = OperationsSession() session.update_entity( - project_name, "workfile", workfile_info["id"], update_data + project_name, + "workfile", + workfile_info["id"], + {"attrib": {"description": note}}, ) session.commit() @@ -617,7 +583,7 @@ class PublishWorkfilesModel: def get_file_items(self, folder_id, task_name): # TODO refactor to use less server API calls project_name = self._controller.get_current_project_name() - # Get subset docs of asset + # Get subset docs of folder product_entities = ayon_api.get_products( project_name, folder_ids=[folder_id], diff --git a/server_addon/max/server/settings/publishers.py b/server_addon/max/server/settings/publishers.py index c23ddced30..5e1b348d92 100644 --- a/server_addon/max/server/settings/publishers.py +++ b/server_addon/max/server/settings/publishers.py @@ -116,6 +116,10 @@ class PublishersModel(BaseSettingsModel): default_factory=ValidateModelNameModel, title="Validate Model Name" ) + ValidateRenderPasses: BasicValidateModel = SettingsField( + default_factory=BasicValidateModel, + title="Validate Render Passes" + ) ExtractModelObj: BasicValidateModel = SettingsField( default_factory=BasicValidateModel, title="Extract OBJ", @@ -185,6 +189,11 @@ DEFAULT_PUBLISH_SETTINGS = { "optional": True, "active": False, }, + "ValidateRenderPasses": { + "enabled": True, + "optional": False, + "active": True + }, "ExtractModelObj": { "enabled": True, "optional": True,