From 9fe4b635174060697a9ea8a9e33f47394a34d9e9 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 001/122] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 69a388de1319eb49de84a0f6d846631623fc5a7d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 002/122] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index a42f889e85..303cd052bb 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From 108597f9b1e139f31e6b0f20568866cb2971020a Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 003/122] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 199aba87727d7a2417d7f8122dd34f6e4160b467 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 004/122] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_entities (list): Asset entities for which want to find data + + Returns: + (dict): collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: return # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From bd884262b0c001715432f28ec1cae6feeeabfed1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 005/122] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From 60cc108251db884a04cef1d2ea29a558a7750b8c Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 006/122] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From aaa1f13f9d0ae038f70eb2cdc21cba56f92b97dd Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 007/122] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From c2aca3422c8c2e29a169f9550e7e1719733f7ec4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 008/122] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 95d3686889470a8ad6d677b949a86cab094e47ea Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 009/122] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From e29d4e5699e6dace616933317c57fcc9bc43c878 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 010/122] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From 28518eeb21f2a9ef56c32c0009ce09aecf871a86 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 011/122] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From b65a1d4e79e3fa2ff4ca11392f9ccbce68a19a78 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 012/122] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From b095249fb859c9845d00efb8d69bd515867c6e94 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 013/122] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From 79c9dc94528ff8f3ae216f106b2225ae790fb044 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 014/122] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From aa7e7093df8d72357118bdb34dbe03e4e73d6801 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 015/122] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From f50999d0927bf533a74417479e4cdb4a06b32b3d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 016/122] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From edb55949df619a81c1828571030634a4b0c49584 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 12:56:24 +0200 Subject: [PATCH 017/122] refactor avalon imports from lib_template_builder --- .../hosts/maya/api/lib_template_builder.py | 184 ++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 openpype/hosts/maya/api/lib_template_builder.py diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py new file mode 100644 index 0000000000..172a6f9b2b --- /dev/null +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -0,0 +1,184 @@ +from collections import OrderedDict +import maya.cmds as cmds + +import qargparse +from openpype.tools.utils.widgets import OptionDialog +from lib import get_main_window, imprint + +# To change as enum +build_types = ["context_asset", "linked_asset", "all_assets"] + + +def get_placeholder_attributes(node): + return { + attr: cmds.getAttr("{}.{}".format(node, attr)) + for attr in cmds.listAttr(node, userDefined=True)} + + +def delete_placeholder_attributes(node): + ''' + function to delete all extra placeholder attributes + ''' + extra_attributes = get_placeholder_attributes(node) + for attribute in extra_attributes: + cmds.deleteAttr(node + '.' + attribute) + + +def create_placeholder(): + args = placeholder_window() + + if not args: + return # operation canceled, no locator created + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] + if selection: + cmds.parent(placeholder, selection[0]) + # custom arg parse to force empty data query + # and still imprint them on placeholder + # and getting items when arg is of type Enumerator + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + # Some tweaks because imprint force enums to to default value so we get + # back arg read and force them to attributes + imprint_enum(placeholder, args) + + # Add helper attributes to keep placeholder info + cmds.addAttr( + placeholder, longName="parent", + hidden=True, dataType="string") + cmds.addAttr( + placeholder, longName="index", + hidden=True, attributeType="short", + defaultValue=-1) + + +def update_placeholder(): + placeholder = cmds.ls(selection=True) + if len(placeholder) == 0: + raise ValueError("No node selected") + if len(placeholder) > 1: + raise ValueError("Too many selected nodes") + placeholder = placeholder[0] + + args = placeholder_window(get_placeholder_attributes(placeholder)) + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + if not args: + return # operation canceled + + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + imprint(placeholder, options) + imprint_enum(placeholder, args) + + +def imprint_enum(placeholder, args): + """ + Imprint method doesn't act properly with enums. + Replacing the functionnality with this for now + """ + enum_values = {str(arg): arg.read() + for arg in args if arg._data.get("items")} + string_to_value_enum_table = { + build: i for i, build + in enumerate(build_types)} + for key, value in enum_values.items(): + cmds.setAttr( + placeholder + "." + key, + string_to_value_enum_table[value]) + + +def placeholder_window(options=None): + options = options or dict() + dialog = OptionDialog(parent=get_main_window()) + dialog.setWindowTitle("Create Placeholder") + + args = [ + qargparse.Separator("Main attributes"), + qargparse.Enum( + "builder_type", + label="Asset Builder Type", + default=options.get("builder_type", 0), + items=build_types, + help="""Asset Builder Type +Builder type describe what template loader will look for. +context_asset : Template loader will look for subsets of +current context asset (Asset bob will find asset) +linked_asset : Template loader will look for assets linked +to current context asset. +Linked asset are looked in avalon database under field "inputLinks" +""" + ), + qargparse.String( + "family", + default=options.get("family", ""), + label="OpenPype Family", + placeholder="ex: model, look ..."), + qargparse.String( + "representation", + default=options.get("representation", ""), + label="OpenPype Representation", + placeholder="ex: ma, abc ..."), + qargparse.String( + "loader", + default=options.get("loader", ""), + label="Loader", + placeholder="ex: ReferenceLoader, LightLoader ...", + help="""Loader +Defines what openpype loader will be used to load assets. +Useable loader depends on current host's loader list. +Field is case sensitive. +"""), + qargparse.String( + "loader_args", + default=options.get("loader_args", ""), + label="Loader Arguments", + placeholder='ex: {"camera":"persp", "lights":True}', + help="""Loader +Defines a dictionnary of arguments used to load assets. +Useable arguments depend on current placeholder Loader. +Field should be a valid python dict. Anything else will be ignored. +"""), + qargparse.Integer( + "order", + default=options.get("order", 0), + min=0, + max=999, + label="Order", + placeholder="ex: 0, 100 ... (smallest order loaded first)", + help="""Order +Order defines asset loading priority (0 to 999) +Priority rule is : "lowest is first to load"."""), + qargparse.Separator( + "Optional attributes"), + qargparse.String( + "asset", + default=options.get("asset", ""), + label="Asset filter", + placeholder="regex filtering by asset name", + help="Filtering assets by matching field regex to asset's name"), + qargparse.String( + "subset", + default=options.get("subset", ""), + label="Subset filter", + placeholder="regex filtering by subset name", + help="Filtering assets by matching field regex to subset's name"), + qargparse.String( + "hierarchy", + default=options.get("hierarchy", ""), + label="Hierarchy filter", + placeholder="regex filtering by asset's hierarchy", + help="Filtering assets by matching field asset's hierarchy") + ] + dialog.create(args) + + if not dialog.exec_(): + return None + + return args From 15e51cd6a640aea61eb927b84ce6b48990d206f3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 14:58:27 +0200 Subject: [PATCH 018/122] add the templated wrokfile build schema for maya --- .../defaults/project_settings/maya.json | 8 +++++ .../projects_schema/schema_project_maya.json | 4 +++ .../schema_templated_workfile_build.json | 29 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index efd22e13c8..2e0e30b74b 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -718,6 +718,14 @@ } ] }, + "templated_workfile_build": { + "profiles": [ + { + "task_types": [], + "path": "/path/to/your/template" + } + ] + }, "filters": { "preset 1": { "ValidateNoAnimation": false, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json index 40e98b0333..d137049e9e 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_maya.json @@ -73,6 +73,10 @@ "type": "schema", "name": "schema_workfile_build" }, + { + "type": "schema", + "name": "schema_templated_workfile_build" + }, { "type": "schema", "name": "schema_publish_gui_filter" diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json new file mode 100644 index 0000000000..01e74f64b0 --- /dev/null +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -0,0 +1,29 @@ +{ + "type": "dict", + "collapsible": true, + "key": "templated_workfile_build", + "label": "Templated Workfile Build Settings", + "children": [ + { + "type": "list", + "key": "profiles", + "label": "Profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "path", + "label": "Path to template", + "type": "text", + "object_type": "text" + } + ] + } + } + ] +} From c8c36144cb26df5d0024fcd02df265736bbd209f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 24 May 2022 17:28:42 +0200 Subject: [PATCH 019/122] add placeholder menu to maya --- .../hosts/maya/api/lib_template_builder.py | 2 +- openpype/hosts/maya/api/menu.py | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 172a6f9b2b..d8772f3f9a 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -3,7 +3,7 @@ import maya.cmds as cmds import qargparse from openpype.tools.utils.widgets import OptionDialog -from lib import get_main_window, imprint +from .lib import get_main_window, imprint # To change as enum build_types = ["context_asset", "linked_asset", "all_assets"] diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 97f06c43af..8beaf491bb 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -11,8 +11,10 @@ from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib + from .lib import get_main_window, IS_HEADLESS from .commands import reset_frame_range +from .lib_template_builder import create_placeholder, update_placeholder log = logging.getLogger(__name__) @@ -139,6 +141,24 @@ def install(): parent_widget ) ) + + builder_menu = cmds.menuItem( + "Template Builder", + subMenu=True, + tearOff=True, + parent=MENU_NAME + ) + cmds.menuItem( + "Create Placeholder", + parent=builder_menu, + command=lambda *args: create_placeholder() + ) + cmds.menuItem( + "Update Placeholder", + parent=builder_menu, + command=lambda *args: update_placeholder() + ) + cmds.setParent(MENU_NAME, menu=True) def add_scripts_menu(): From 770b6d3ab2ee9e3bdf460cee4fdba96d67e44fb2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:25:39 +0200 Subject: [PATCH 020/122] setup build template in openpype lib --- openpype/lib/__init__.py | 2 + openpype/lib/abstract_template_loader.py | 447 ++++++++++++++++++++++ openpype/lib/avalon_context.py | 222 +++++------ openpype/lib/build_template.py | 61 +++ openpype/lib/build_template_exceptions.py | 35 ++ 5 files changed, 660 insertions(+), 107 deletions(-) create mode 100644 openpype/lib/abstract_template_loader.py create mode 100644 openpype/lib/build_template.py create mode 100644 openpype/lib/build_template_exceptions.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 8d4e733b7d..8f3919d378 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -136,6 +136,7 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, + get_loaders_by_name, BuildWorkfile, @@ -308,6 +309,7 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", + "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py new file mode 100644 index 0000000000..6888cbf757 --- /dev/null +++ b/openpype/lib/abstract_template_loader.py @@ -0,0 +1,447 @@ +import os +from abc import ABCMeta, abstractmethod + +import traceback + +import six + +import openpype +from openpype.settings import get_project_settings +from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name +from openpype.api import PypeLogger as Logger +from openpype.pipeline import legacy_io + +from functools import reduce + +from openpype.lib.build_template_exceptions import ( + TemplateAlreadyImported, + TemplateLoadingFailed, + TemplateProfileNotFound, + TemplateNotFound +) + + +def update_representations(entities, entity): + if entity['context']['subset'] not in entities: + entities[entity['context']['subset']] = entity + else: + current = entities[entity['context']['subset']] + incomming = entity + entities[entity['context']['subset']] = max( + current, incomming, + key=lambda entity: entity["context"].get("version", -1)) + + return entities + + +def parse_loader_args(loader_args): + if not loader_args: + return dict() + try: + parsed_args = eval(loader_args) + if not isinstance(parsed_args, dict): + return dict() + else: + return parsed_args + except Exception as err: + print( + "Error while parsing loader arguments '{}'.\n{}: {}\n\n" + "Continuing with default arguments. . .".format( + loader_args, + err.__class__.__name__, + err)) + return dict() + + +@six.add_metaclass(ABCMeta) +class AbstractTemplateLoader: + """ + Abstraction of Template Loader. + Properties: + template_path : property to get current template path + Methods: + import_template : Abstract Method. Used to load template, + depending on current host + get_template_nodes : Abstract Method. Used to query nodes acting + as placeholders. Depending on current host + """ + + def __init__(self, placeholder_class): + + self.loaders_by_name = get_loaders_by_name() + self.current_asset = legacy_io.Session["AVALON_ASSET"] + self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.host_name = legacy_io.Session["AVALON_APP"] + self.task_name = legacy_io.Session["AVALON_TASK"] + self.placeholder_class = placeholder_class + self.current_asset_docs = legacy_io.find_one({ + "type": "asset", + "name": self.current_asset + }) + self.task_type = ( + self.current_asset_docs + .get("data", {}) + .get("tasks", {}) + .get(self.task_name, {}) + .get("type") + ) + + self.log = Logger().get_logger("BUILD TEMPLATE") + + self.log.info( + "BUILDING ASSET FROM TEMPLATE :\n" + "Starting templated build for {asset} in {project}\n\n" + "Asset : {asset}\n" + "Task : {task_name} ({task_type})\n" + "Host : {host}\n" + "Project : {project}\n".format( + asset=self.current_asset, + host=self.host_name, + project=self.project_name, + task_name=self.task_name, + task_type=self.task_type + )) + # Skip if there is no loader + if not self.loaders_by_name: + self.log.warning( + "There is no registered loaders. No assets will be loaded") + return + + def template_already_imported(self, err_msg): + """In case template was already loaded. + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case.""" + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateAlreadyImported(err_msg) + + def template_loading_failed(self, err_msg): + """In case template loading failed + Raise the error as a default action. + Override this method in your template loader implementation + to manage this case. + """ + self.log.error("{}: {}".format( + err_msg.__class__.__name__, + err_msg)) + raise TemplateLoadingFailed(err_msg) + + @property + def template_path(self): + """ + Property returning template path. Avoiding setter. + Getting template path from open pype settings based on current avalon + session and solving the path variables if needed. + Returns: + str: Solved template path + Raises: + TemplateProfileNotFound: No profile found from settings for + current avalon session + KeyError: Could not solve path because a key does not exists + in avalon context + TemplateNotFound: Solved path does not exists on current filesystem + """ + project_name = self.project_name + host_name = self.host_name + task_name = self.task_name + task_type = self.task_type + + anatomy = Anatomy(project_name) + project_settings = get_project_settings(project_name) + + build_info = project_settings[host_name]['templated_workfile_build'] + profiles = build_info['profiles'] + + for prf in profiles: + if prf['task_types'] and task_type not in prf['task_types']: + continue + if prf['task_names'] and task_name not in prf['task_names']: + continue + path = prf['path'] + break + else: # IF no template were found (no break happened) + raise TemplateProfileNotFound( + "No matching profile found for task '{}' of type '{}' " + "with host '{}'".format(task_name, task_type, host_name) + ) + if path is None: + raise TemplateLoadingFailed( + "Template path is not set.\n" + "Path need to be set in {}\\Template Workfile Build " + "Settings\\Profiles".format(host_name.title())) + try: + solved_path = None + while True: + solved_path = anatomy.path_remapper(path) + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + finally: + solved_path = os.path.normpath(solved_path) + + if not os.path.exists(solved_path): + raise TemplateNotFound( + "Template found in openPype settings for task '{}' with host " + "'{}' does not exists. (Not found : {})".format( + task_name, host_name, solved_path)) + + self.log.info("Found template at : '{}'".format(solved_path)) + + return solved_path + + def populate_template(self, ignored_ids=None): + """ + Use template placeholders to load assets and parent them in hierarchy + Arguments : + ignored_ids : + Returns: + None + """ + loaders_by_name = self.loaders_by_name + current_asset = self.current_asset + linked_assets = [asset['name'] for asset + in get_linked_assets(self.current_asset_docs)] + + ignored_ids = ignored_ids or [] + placeholders = self.get_placeholders() + for placeholder in placeholders: + placeholder_representations = self.get_placeholder_representations( + placeholder, + current_asset, + linked_assets + ) + for representation in placeholder_representations: + + self.preload(placeholder, loaders_by_name, representation) + + if self.load_data_is_incorrect( + placeholder, + representation, + ignored_ids): + continue + + self.log.info( + "Loading {}_{} with loader {}\n" + "Loader arguments used : {}".format( + representation['context']['asset'], + representation['context']['subset'], + placeholder.loader, + placeholder.data['loader_args'])) + + try: + container = self.load( + placeholder, loaders_by_name, representation) + except Exception: + self.load_failed(placeholder, representation) + else: + self.load_succeed(placeholder, container) + finally: + self.postload(placeholder) + + def get_placeholder_representations( + self, placeholder, current_asset, linked_assets): + placeholder_db_filters = placeholder.convert_to_db_filters( + current_asset, + linked_assets) + # get representation by assets + for db_filter in placeholder_db_filters: + placeholder_representations = list(avalon.io.find(db_filter)) + for representation in reduce(update_representations, + placeholder_representations, + dict()).values(): + yield representation + + def load_data_is_incorrect( + self, placeholder, last_representation, ignored_ids): + if not last_representation: + self.log.warning(placeholder.err_message()) + return True + if (str(last_representation['_id']) in ignored_ids): + print("Ignoring : ", last_representation['_id']) + return True + return False + + def preload(self, placeholder, loaders_by_name, last_representation): + pass + + def load(self, placeholder, loaders_by_name, last_representation): + return openpype.pipeline.load( + loaders_by_name[placeholder.loader], + last_representation['_id'], + options=parse_loader_args(placeholder.data['loader_args'])) + + def load_succeed(self, placeholder, container): + placeholder.parent_in_hierarchy(container) + + def load_failed(self, placeholder, last_representation): + self.log.warning("Got error trying to load {}:{} with {}\n\n" + "{}".format(last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader, + traceback.format_exc())) + + def postload(self, placeholder): + placeholder.clean() + + def update_missing_containers(self): + loaded_containers_ids = self.get_loaded_containers_by_id() + self.populate_template(ignored_ids=loaded_containers_ids) + + def get_placeholders(self): + placeholder_class = self.placeholder_class + placeholders = map(placeholder_class, self.get_template_nodes()) + valid_placeholders = filter(placeholder_class.is_valid, placeholders) + sorted_placeholders = sorted(valid_placeholders, + key=placeholder_class.order) + return sorted_placeholders + + @abstractmethod + def get_loaded_containers_by_id(self): + """ + Collect already loaded containers for updating scene + Return: + dict (string, node): A dictionnary id as key + and containers as value + """ + pass + + @abstractmethod + def import_template(self, template_path): + """ + Import template in current host + Args: + template_path (str): fullpath to current task and + host's template file + Return: + None + """ + pass + + @abstractmethod + def get_template_nodes(self): + """ + Returning a list of nodes acting as host placeholders for + templating. The data representation is by user. + AbstractLoadTemplate (and LoadTemplate) won't directly manipulate nodes + Args : + None + Returns: + list(AnyNode): Solved template path + """ + pass + + +@six.add_metaclass(ABCMeta) +class AbstractPlaceholder: + """Abstraction of placeholders logic + Properties: + attributes: A list of mandatory attribute to decribe placeholder + and assets to load. + optional_attributes: A list of optional attribute to decribe + placeholder and assets to load + loader: Name of linked loader to use while loading assets + is_context: Is placeholder linked + to context asset (or to linked assets) + Methods: + is_repres_valid: + loader: + order: + is_valid: + get_data: + parent_in_hierachy: + """ + + attributes = {'builder_type', 'op_family', 'op_representation', + 'order', 'loader', 'loader_args'} + optional_attributes = {} + + def __init__(self, node): + self.get_data(node) + + def order(self): + """Get placeholder order. + Order is used to sort them by priority + Priority is lowset first, highest last + (ex: + 1: First to load + 100: Last to load) + Returns: + Int: Order priority + """ + return self.data.get('order') + + @property + def loader(self): + """Return placeholder loader type + Returns: + string: Loader name + """ + return self.data.get('loader') + + @property + def is_context(self): + """Return placeholder type + context_asset: For loading current asset + linked_asset: For loading linked assets + Returns: + bool: true if placeholder is a context placeholder + """ + return self.data.get('builder_type') == 'context_asset' + + def is_valid(self): + """Test validity of placeholder + i.e.: every attributes exists in placeholder data + Returns: + Bool: True if every attributes are a key of data + """ + if set(self.attributes).issubset(self.data.keys()): + print("Valid placeholder : {}".format(self.data["node"])) + return True + print("Placeholder is not valid : {}".format(self.data["node"])) + return False + + @abstractmethod + def parent_in_hierarchy(self, containers): + """Place container in correct hierarchy + given by placeholder + Args: + containers (String): Container name returned back by + placeholder's loader. + """ + pass + + @abstractmethod + def clean(self): + """Clean placeholder from hierarchy after loading assets. + """ + pass + + @abstractmethod + def convert_to_db_filters(self, current_asset, linked_asset): + """map current placeholder data as a db filter + args: + current_asset (String): Name of current asset in context + linked asset (list[String]) : Names of assets linked to + current asset in context + Returns: + dict: a dictionnary describing a filter to look for asset in + a database + """ + pass + + @abstractmethod + def get_data(self, node): + """ + Collect placeholders information. + Args: + node (AnyNode): A unique node decided by Placeholder implementation + """ + pass diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9d8a92cfe9..8c80b4a4ae 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -15,6 +15,7 @@ from openpype.settings import ( get_project_settings, get_system_settings ) + from .anatomy import Anatomy from .profiles_filtering import filter_profiles from .events import emit_event @@ -922,6 +923,118 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): ) +@with_pipeline_io +def collect_last_version_repres(asset_entities): + """Collect subsets, versions and representations for asset_entities. + + Args: + asset_entities (list): Asset entities for which want to find data + + Returns: + (dict): collected entities + + Example output: + ``` + { + {Asset ID}: { + "asset_entity": , + "subsets": { + {Subset ID}: { + "subset_entity": , + "version": { + "version_entity": , + "repres": [ + , , ... + ] + } + }, + ... + } + }, + ... + } + output[asset_id]["subsets"][subset_id]["version"]["repres"] + ``` + """ + + if not asset_entities: + return {} + + asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + + subsets = list(legacy_io.find({ + "type": "subset", + "parent": {"$in": list(asset_entity_by_ids.keys())} + })) + subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} + + sorted_versions = list(legacy_io.find({ + "type": "version", + "parent": {"$in": list(subset_entity_by_ids.keys())} + }).sort("name", -1)) + + subset_id_with_latest_version = [] + last_versions_by_id = {} + for version in sorted_versions: + subset_id = version["parent"] + if subset_id in subset_id_with_latest_version: + continue + subset_id_with_latest_version.append(subset_id) + last_versions_by_id[version["_id"]] = version + + repres = legacy_io.find({ + "type": "representation", + "parent": {"$in": list(last_versions_by_id.keys())} + }) + + output = {} + for repre in repres: + version_id = repre["parent"] + version = last_versions_by_id[version_id] + + subset_id = version["parent"] + subset = subset_entity_by_ids[subset_id] + + asset_id = subset["parent"] + asset = asset_entity_by_ids[asset_id] + + if asset_id not in output: + output[asset_id] = { + "asset_entity": asset, + "subsets": {} + } + + if subset_id not in output[asset_id]["subsets"]: + output[asset_id]["subsets"][subset_id] = { + "subset_entity": subset, + "version": { + "version_entity": version, + "repres": [] + } + } + + output[asset_id]["subsets"][subset_id]["version"]["repres"].append( + repre + ) + + return output + + +@with_pipeline_io +def get_loaders_by_name(): + from openpype.pipeline import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + class BuildWorkfile: """Wrapper for build workfile process. @@ -979,8 +1092,6 @@ class BuildWorkfile: ... }] """ - from openpype.pipeline import discover_loader_plugins - # Get current asset name and entity current_asset_name = legacy_io.Session["AVALON_ASSET"] current_asset_entity = legacy_io.find_one({ @@ -996,14 +1107,7 @@ class BuildWorkfile: return # Prepare available loaders - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {0}!".format(loader_name) - ) - loaders_by_name[loader_name] = loader + loaders_by_name = get_loaders_by_name() # Skip if there are any loaders if not loaders_by_name: @@ -1075,7 +1179,7 @@ class BuildWorkfile: return # Prepare entities from database for assets - prepared_entities = self._collect_last_version_repres(assets) + prepared_entities = collect_last_version_repres(assets) # Load containers by prepared entities and presets loaded_containers = [] @@ -1491,102 +1595,6 @@ class BuildWorkfile: return loaded_containers - @with_pipeline_io - def _collect_last_version_repres(self, asset_entities): - """Collect subsets, versions and representations for asset_entities. - - Args: - asset_entities (list): Asset entities for which want to find data - - Returns: - (dict): collected entities - - Example output: - ``` - { - {Asset ID}: { - "asset_entity": , - "subsets": { - {Subset ID}: { - "subset_entity": , - "version": { - "version_entity": , - "repres": [ - , , ... - ] - } - }, - ... - } - }, - ... - } - output[asset_id]["subsets"][subset_id]["version"]["repres"] - ``` - """ - - if not asset_entities: - return {} - - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} - - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) - subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) - - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version - - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) - - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] - - if asset_id not in output: - output[asset_id] = { - "asset_entity": asset, - "subsets": {} - } - - if subset_id not in output[asset_id]["subsets"]: - output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, - "version": { - "version_entity": version, - "repres": [] - } - } - - output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre - ) - - return output - @with_pipeline_io def get_creator_by_name(creator_name, case_sensitive=False): diff --git a/openpype/lib/build_template.py b/openpype/lib/build_template.py new file mode 100644 index 0000000000..7f749cbec2 --- /dev/null +++ b/openpype/lib/build_template.py @@ -0,0 +1,61 @@ +from openpype.pipeline import registered_host +from openpype.lib import classes_from_module +from importlib import import_module + +from .abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader) + +from .build_template_exceptions import ( + TemplateLoadingFailed, + TemplateAlreadyImported, + MissingHostTemplateModule, + MissingTemplatePlaceholderClass, + MissingTemplateLoaderClass +) + +_module_path_format = 'openpype.{host}.template_loader' + + +def build_workfile_template(*args): + template_loader = build_template_loader() + try: + template_loader.import_template(template_loader.template_path) + except TemplateAlreadyImported as err: + template_loader.template_already_imported(err) + except TemplateLoadingFailed as err: + template_loader.template_loading_failed(err) + else: + template_loader.populate_template() + + +def update_workfile_template(args): + template_loader = build_template_loader() + template_loader.update_missing_containers() + + +def build_template_loader(): + host_name = registered_host().__name__.partition('.')[2] + module_path = _module_path_format.format(host=host_name) + module = import_module(module_path) + if not module: + raise MissingHostTemplateModule( + "No template loader found for host {}".format(host_name)) + + template_loader_class = classes_from_module( + AbstractTemplateLoader, + module + ) + template_placeholder_class = classes_from_module( + AbstractPlaceholder, + module + ) + + if not template_loader_class: + raise MissingTemplateLoaderClass() + template_loader_class = template_loader_class[0] + + if not template_placeholder_class: + raise MissingTemplatePlaceholderClass() + template_placeholder_class = template_placeholder_class[0] + return template_loader_class(template_placeholder_class) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py new file mode 100644 index 0000000000..d781eff204 --- /dev/null +++ b/openpype/lib/build_template_exceptions.py @@ -0,0 +1,35 @@ +class MissingHostTemplateModule(Exception): + """Error raised when expected module does not exists""" + pass + + +class MissingTemplatePlaceholderClass(Exception): + """Error raised when module doesn't implement a placeholder class""" + pass + + +class MissingTemplateLoaderClass(Exception): + """Error raised when module doesn't implement a template loader class""" + pass + + +class TemplateNotFound(Exception): + """Exception raised when template does not exist.""" + pass + + +class TemplateProfileNotFound(Exception): + """Exception raised when current profile + doesn't match any template profile""" + pass + + +class TemplateAlreadyImported(Exception): + """Error raised when Template was already imported by host for + this session""" + pass + + +class TemplateLoadingFailed(Exception): + """Error raised whend Template loader was unable to load the template""" + pass \ No newline at end of file From a5a3685f2b5b99bbd1f8de78581eb17af0175ed3 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 12:52:44 +0200 Subject: [PATCH 021/122] add template loader module --- openpype/hosts/maya/api/template_loader.py | 242 +++++++++++++++++++++ 1 file changed, 242 insertions(+) create mode 100644 openpype/hosts/maya/api/template_loader.py diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py new file mode 100644 index 0000000000..0e346ca411 --- /dev/null +++ b/openpype/hosts/maya/api/template_loader.py @@ -0,0 +1,242 @@ +from maya import cmds + +from openpype.pipeline import legacy_io +from openpype.lib.abstract_template_loader import ( + AbstractPlaceholder, + AbstractTemplateLoader +) +from openpype.lib.build_template_exceptions import TemplateAlreadyImported + +PLACEHOLDER_SET = 'PLACEHOLDERS_SET' + + +class MayaTemplateLoader(AbstractTemplateLoader): + """Concrete implementation of AbstractTemplateLoader for maya + """ + + def import_template(self, path): + """Import template into current scene. + Block if a template is already loaded. + Args: + path (str): A path to current template (usually given by + get_template_path implementation) + Returns: + bool: Wether the template was succesfully imported or not + """ + if cmds.objExists(PLACEHOLDER_SET): + raise TemplateAlreadyImported( + "Build template already loaded\n" + "Clean scene if needed (File > New Scene)") + + cmds.sets(name=PLACEHOLDER_SET, empty=True) + self.new_nodes = cmds.file(path, i=True, returnNewNodes=True) + cmds.setAttr(PLACEHOLDER_SET + '.hiddenInOutliner', True) + + for set in cmds.listSets(allSets=True): + if (cmds.objExists(set) and + cmds.attributeQuery('id', node=set, exists=True) and + cmds.getAttr(set + '.id') == 'pyblish.avalon.instance'): + if cmds.attributeQuery('asset', node=set, exists=True): + cmds.setAttr( + set + '.asset', + legacy_io.Session['AVALON_ASSET'], type='string' + ) + + return True + + def template_already_imported(self, err_msg): + clearButton = "Clear scene and build" + updateButton = "Update template" + abortButton = "Abort" + + title = "Scene already builded" + message = ( + "It's seems a template was already build for this scene.\n" + "Error message reveived :\n\n\"{}\"".format(err_msg)) + buttons = [clearButton, updateButton, abortButton] + defaultButton = clearButton + cancelButton = abortButton + dismissString = abortButton + answer = cmds.confirmDialog( + t=title, + m=message, + b=buttons, + db=defaultButton, + cb=cancelButton, + ds=dismissString) + + if answer == clearButton: + cmds.file(newFile=True, force=True) + self.import_template(self.template_path) + self.populate_template() + elif answer == updateButton: + self.update_missing_containers() + elif answer == abortButton: + return + + @staticmethod + def get_template_nodes(): + attributes = cmds.ls('*.builder_type', long=True) + return [attribute.rpartition('.')[0] for attribute in attributes] + + def get_loaded_containers_by_id(self): + containers = cmds.sets('AVALON_CONTAINERS', q=True) + return [ + cmds.getAttr(container + '.representation') + for container in containers] + + +class MayaPlaceholder(AbstractPlaceholder): + """Concrete implementation of AbstractPlaceholder for maya + """ + + optional_attributes = {'asset', 'subset', 'hierarchy'} + + def get_data(self, node): + user_data = dict() + for attr in self.attributes.union(self.optional_attributes): + attribute_name = '{}.{}'.format(node, attr) + if not cmds.attributeQuery(attr, node=node, exists=True): + print("{} not found".format(attribute_name)) + continue + user_data[attr] = cmds.getAttr( + attribute_name, + asString=True) + user_data['parent'] = ( + cmds.getAttr(node + '.parent', asString=True) + or node.rpartition('|')[0] or "") + user_data['node'] = node + if user_data['parent']: + siblings = cmds.listRelatives(user_data['parent'], children=True) + else: + siblings = cmds.ls(assemblies=True) + node_shortname = user_data['node'].rpartition('|')[2] + current_index = cmds.getAttr(node + '.index', asString=True) + user_data['index'] = ( + current_index if current_index >= 0 + else siblings.index(node_shortname)) + + self.data = user_data + + def parent_in_hierarchy(self, containers): + """Parent loaded container to placeholder's parent + ie : Set loaded content as placeholder's sibling + Args: + containers (String): Placeholder loaded containers + """ + if not containers: + return + + roots = cmds.sets(containers, q=True) + nodes_to_parent = [] + for root in roots: + if root.endswith("_RN"): + refRoot = cmds.referenceQuery(root, n=True)[0] + refRoot = cmds.listRelatives(refRoot, parent=True) or [refRoot] + nodes_to_parent.extend(refRoot) + elif root in cmds.listSets(allSets=True): + if not cmds.sets(root, q=True): + return + else: + continue + else: + nodes_to_parent.append(root) + + if self.data['parent']: + cmds.parent(nodes_to_parent, self.data['parent']) + # Move loaded nodes to correct index in outliner hierarchy + placeholder_node = self.data['node'] + placeholder_form = cmds.xform( + placeholder_node, + q=True, + matrix=True, + worldSpace=True + ) + for node in set(nodes_to_parent): + cmds.reorder(node, front=True) + cmds.reorder(node, relative=self.data['index']) + cmds.xform(node, matrix=placeholder_form, ws=True) + + holding_sets = cmds.listSets(object=placeholder_node) + if not holding_sets: + return + for holding_set in holding_sets: + cmds.sets(roots, forceElement=holding_set) + + def clean(self): + """Hide placeholder, parent them to root + add them to placeholder set and register placeholder's parent + to keep placeholder info available for future use + """ + node = self.data['node'] + if self.data['parent']: + cmds.setAttr(node + '.parent', self.data['parent'], type='string') + if cmds.getAttr(node + '.index') < 0: + cmds.setAttr(node + '.index', self.data['index']) + + holding_sets = cmds.listSets(object=node) + if holding_sets: + for set in holding_sets: + cmds.sets(node, remove=set) + + if cmds.listRelatives(node, p=True): + node = cmds.parent(node, world=True)[0] + cmds.sets(node, addElement=PLACEHOLDER_SET) + cmds.hide(node) + cmds.setAttr(node + '.hiddenInOutliner', True) + + def convert_to_db_filters(self, current_asset, linked_asset): + if self.data['builder_type'] == "context_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": current_asset, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + elif self.data['builder_type'] == "linked_asset": + return [ + { + "type": "representation", + "context.asset": { + "$eq": asset_name, + "$regex": self.data['asset'] + }, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } for asset_name in linked_asset + ] + + else: + return [ + { + "type": "representation", + "context.asset": {"$regex": self.data['asset']}, + "context.subset": {"$regex": self.data['subset']}, + "context.hierarchy": {"$regex": self.data['hierarchy']}, + "context.representation": self.data['representation'], + "context.family": self.data['family'], + } + ] + + def err_message(self): + return ( + "Error while trying to load a representation.\n" + "Either the subset wasn't published or the template is malformed." + "\n\n" + "Builder was looking for :\n{attributes}".format( + attributes="\n".join([ + "{}: {}".format(key.title(), value) + for key, value in self.data.items()] + ) + ) + ) From f2ae0ffa5950d922fd3cb90ce8bbf30ec64ca0b7 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 14:28:28 +0200 Subject: [PATCH 022/122] add build workfile in menu --- openpype/hosts/maya/api/menu.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 8beaf491bb..c66eeb449f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,7 +6,13 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import BuildWorkfile +from openpype.api import ( + BuildWorkfile, + # build_workfile_template + # update_workfile_template +) + +from openpype.lib.build_template import build_workfile_template, update_workfile_template from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools @@ -158,6 +164,16 @@ def install(): parent=builder_menu, command=lambda *args: update_placeholder() ) + cmds.menuItem( + "Build Workfile from template", + parent=builder_menu, + command=lambda *args: build_workfile_template() + ) + cmds.menuItem( + "Update Workfile from template", + parent=builder_menu, + command=lambda *args: update_workfile_template() + ) cmds.setParent(MENU_NAME, menu=True) From 41a47bb2bfb9b728f0cad37f5614e5d382b2d9d1 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:35:05 +0200 Subject: [PATCH 023/122] delete the task_name verification since it does not exists in the maya menu settings --- openpype/lib/abstract_template_loader.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 6888cbf757..2dfec1a006 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -157,8 +157,6 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue - if prf['task_names'] and task_name not in prf['task_names']: - continue path = prf['path'] break else: # IF no template were found (no break happened) @@ -253,7 +251,7 @@ class AbstractTemplateLoader: linked_assets) # get representation by assets for db_filter in placeholder_db_filters: - placeholder_representations = list(avalon.io.find(db_filter)) + placeholder_representations = list(legacy_io.find(db_filter)) for representation in reduce(update_representations, placeholder_representations, dict()).values(): From 58814d21e4688fbb13d183fab7ba9010c68b57f8 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 25 May 2022 16:38:47 +0200 Subject: [PATCH 024/122] rename correctly attributes to correpsond the ones in the placeholders --- openpype/lib/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 2dfec1a006..628d0bd895 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -357,7 +357,7 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'op_family', 'op_representation', + attributes = {'builder_type', 'family', 'representation', 'order', 'loader', 'loader_args'} optional_attributes = {} From 6cb037d3d63290752bacc0aa8c2b81cac8e3b370 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Fri, 27 May 2022 12:44:51 +0200 Subject: [PATCH 025/122] create placeholder name dynamically from arguments --- .../hosts/maya/api/lib_template_builder.py | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index d8772f3f9a..ee78f19a3e 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -1,3 +1,4 @@ +import json from collections import OrderedDict import maya.cmds as cmds @@ -30,17 +31,20 @@ def create_placeholder(): if not args: return # operation canceled, no locator created - selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name="_TEMPLATE_PLACEHOLDER_")[0] - if selection: - cmds.parent(placeholder, selection[0]) # custom arg parse to force empty data query # and still imprint them on placeholder # and getting items when arg is of type Enumerator - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() + options = create_options(args) + + # create placeholder name dynamically from args and options + placeholder_name = create_placeholder_name(args, options) + + selection = cmds.ls(selection=True) + placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + + if selection: + cmds.parent(placeholder, selection[0]) + imprint(placeholder, options) # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes @@ -49,13 +53,42 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( placeholder, longName="parent", - hidden=True, dataType="string") + hidden=False, dataType="string") cmds.addAttr( placeholder, longName="index", - hidden=True, attributeType="short", + hidden=False, attributeType="short", defaultValue=-1) +def create_options(args): + options = OrderedDict() + for arg in args: + if not type(arg) == qargparse.Separator: + options[str(arg)] = arg._data.get("items") or arg.read() + return options + + +def create_placeholder_name(args, options): + placeholder_builder_type = [ + arg.read() for arg in args if 'builder_type' in str(arg) + ][0] + placeholder_family = options['family'] + placeholder_name = placeholder_builder_type.split('_') + placeholder_name.insert(1, placeholder_family) + + # add loader arguments if any + if options['loader_args']: + pos = 2 + loader_args = options['loader_args'].replace('\'', '\"') + loader_args = json.loads(loader_args) + values = [v for v in loader_args.values()] + for i in range(len(values)): + placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) + + return placeholder_name + + def update_placeholder(): placeholder = cmds.ls(selection=True) if len(placeholder) == 0: From aa88ee13c0d3b647dc9c11534ed4a742168b0e1d Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:19:12 +0200 Subject: [PATCH 026/122] minor refactoring --- .../hosts/maya/api/lib_template_builder.py | 19 ++++++++++++++----- openpype/hosts/maya/api/menu.py | 11 +++++------ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index ee78f19a3e..bec0f1fc66 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -52,12 +52,21 @@ def create_placeholder(): # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, longName="parent", - hidden=False, dataType="string") + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) cmds.addAttr( - placeholder, longName="index", - hidden=False, attributeType="short", - defaultValue=-1) + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + parents = cmds.ls(selection[0], long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") def create_options(args): diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c66eeb449f..1337713561 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -6,13 +6,12 @@ from Qt import QtWidgets, QtGui import maya.utils import maya.cmds as cmds -from openpype.api import ( - BuildWorkfile, - # build_workfile_template - # update_workfile_template -) +from openpype.api import BuildWorkfile -from openpype.lib.build_template import build_workfile_template, update_workfile_template +from openpype.lib.build_template import ( + build_workfile_template, + update_workfile_template +) from openpype.settings import get_project_settings from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools From d8edf2b1aa9e83861bad1b8aef6da69cc6011de4 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:20:31 +0200 Subject: [PATCH 027/122] change load method since avalon doesn't exsist anymore --- openpype/lib/abstract_template_loader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 628d0bd895..77ba04c4db 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -5,11 +5,10 @@ import traceback import six -import openpype from openpype.settings import get_project_settings from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io +from openpype.pipeline import legacy_io, load from functools import reduce @@ -271,9 +270,10 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - return openpype.pipeline.load( + repre = load.get_representation_context(last_representation) + return load.load_with_repre_context( loaders_by_name[placeholder.loader], - last_representation['_id'], + repre, options=parse_loader_args(placeholder.data['loader_args'])) def load_succeed(self, placeholder, container): From bae9eef400e2b1195d8ece023543ca8d89c83b1b Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 30 May 2022 14:53:49 +0200 Subject: [PATCH 028/122] fix update placeholder --- .../hosts/maya/api/lib_template_builder.py | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bec0f1fc66..2efc210d10 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,14 +69,6 @@ def create_placeholder(): cmds.setAttr(placeholder + '.parent', parents[0], type="string") -def create_options(args): - options = OrderedDict() - for arg in args: - if not type(arg) == qargparse.Separator: - options[str(arg)] = arg._data.get("items") or arg.read() - return options - - def create_placeholder_name(args, options): placeholder_builder_type = [ arg.read() for arg in args if 'builder_type' in str(arg) @@ -112,12 +104,38 @@ def update_placeholder(): if not args: return # operation canceled + options = create_options(args) + + imprint(placeholder, options) + imprint_enum(placeholder, args) + + cmds.addAttr( + placeholder, + longName="parent", + hidden=False, + dataType="string" + ) + cmds.addAttr( + placeholder, + longName="index", + hidden=False, + attributeType="short", + defaultValue=-1 + ) + + selected = cmds.ls(selection=True, long=True) + selected = selected[0].split('|')[-2] + selected = cmds.ls(selected) + parents = cmds.ls(selected, long=True) + cmds.setAttr(placeholder + '.parent', parents[0], type="string") + + +def create_options(args): options = OrderedDict() for arg in args: if not type(arg) == qargparse.Separator: options[str(arg)] = arg._data.get("items") or arg.read() - imprint(placeholder, options) - imprint_enum(placeholder, args) + return options def imprint_enum(placeholder, args): From 349d57a4a8ec86364d64b02798c7579f6c3cb5c2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 10:40:44 +0200 Subject: [PATCH 029/122] change menu command for build and update workfile from template --- openpype/hosts/maya/api/menu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index 1337713561..c0bad7092f 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -166,12 +166,12 @@ def install(): cmds.menuItem( "Build Workfile from template", parent=builder_menu, - command=lambda *args: build_workfile_template() + command=build_workfile_template ) cmds.menuItem( "Update Workfile from template", parent=builder_menu, - command=lambda *args: update_workfile_template() + command=update_workfile_template ) cmds.setParent(MENU_NAME, menu=True) From e2506d569adf78c74ba6452643fec5d1afca0ab2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 2 Jun 2022 12:22:06 +0200 Subject: [PATCH 030/122] get full name placeholder to avoid any conflict between two placeholders with same short name --- .../hosts/maya/api/lib_template_builder.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 2efc210d10..108988a676 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,33 +40,37 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) - placeholder = cmds.spaceLocator(name=placeholder_name.capitalize())[0] + placeholder = cmds.spaceLocator(name=placeholder_name)[0] + + # get the long name of the placeholder (with the groups) + placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) - imprint(placeholder, options) + imprint(placeholder_full_name, options) + # Some tweaks because imprint force enums to to default value so we get # back arg read and force them to attributes - imprint_enum(placeholder, args) + imprint_enum(placeholder_full_name, args) # Add helper attributes to keep placeholder info cmds.addAttr( - placeholder, + placeholder_full_name, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( - placeholder, + placeholder_full_name, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") def create_placeholder_name(args, options): @@ -75,7 +79,10 @@ def create_placeholder_name(args, options): ][0] placeholder_family = options['family'] placeholder_name = placeholder_builder_type.split('_') - placeholder_name.insert(1, placeholder_family) + + # add famlily in any + if placeholder_family: + placeholder_name.insert(1, placeholder_family) # add loader arguments if any if options['loader_args']: @@ -85,9 +92,10 @@ def create_placeholder_name(args, options): values = [v for v in loader_args.values()] for i in range(len(values)): placeholder_name.insert(i + pos, values[i]) + placeholder_name = '_'.join(placeholder_name) - return placeholder_name + return placeholder_name.capitalize() def update_placeholder(): @@ -112,13 +120,13 @@ def update_placeholder(): cmds.addAttr( placeholder, longName="parent", - hidden=False, + hidden=True, dataType="string" ) cmds.addAttr( placeholder, longName="index", - hidden=False, + hidden=True, attributeType="short", defaultValue=-1 ) From a6d948aa93e2c84c001a109c50dede9b9c160321 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:46:24 +0200 Subject: [PATCH 031/122] add a log if no reprensation found for the current placeholder --- openpype/lib/abstract_template_loader.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 77ba04c4db..cd0416426c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -19,6 +19,10 @@ from openpype.lib.build_template_exceptions import ( TemplateNotFound ) +import logging + +log = logging.getLogger(__name__) + def update_representations(entities, entity): if entity['context']['subset'] not in entities: @@ -215,8 +219,15 @@ class AbstractTemplateLoader: current_asset, linked_assets ) - for representation in placeholder_representations: + if not placeholder_representations: + self.log.info( + "There's no representation for this placeholder: " + "{}".format(placeholder.data['node']) + ) + continue + + for representation in placeholder_representations: self.preload(placeholder, loaders_by_name, representation) if self.load_data_is_incorrect( From d6543bf281a418fe768059a58a1a9eb8257ef68f Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Wed, 8 Jun 2022 12:53:59 +0200 Subject: [PATCH 032/122] add debug logs for placeholders --- openpype/lib/abstract_template_loader.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index cd0416426c..159d5c8f6c 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -213,7 +213,13 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() + self.log.debug("Placeholders found in template: {}".format( + [placeholder.data['node'] for placeholder in placeholders] + )) for placeholder in placeholders: + self.log.debug("Start to processing placeholder {}".format( + placeholder.data['node'] + )) placeholder_representations = self.get_placeholder_representations( placeholder, current_asset, From 4a02dd039de637398aa6c2a1d2c26ba772f720da Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Thu, 9 Jun 2022 14:51:48 +0200 Subject: [PATCH 033/122] set empty placholder parent at creation --- openpype/hosts/maya/api/lib_template_builder.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 108988a676..20f6f041fb 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -69,8 +69,7 @@ def create_placeholder(): defaultValue=-1 ) - parents = cmds.ls(selection[0], long=True) - cmds.setAttr(placeholder_full_name + '.parent', parents[0], type="string") + cmds.setAttr(placeholder_full_name + '.parent', "", type="string") def create_placeholder_name(args, options): @@ -131,11 +130,11 @@ def update_placeholder(): defaultValue=-1 ) - selected = cmds.ls(selection=True, long=True) + """selected = cmds.ls(selection=True, long=True) selected = selected[0].split('|')[-2] selected = cmds.ls(selected) - parents = cmds.ls(selected, long=True) - cmds.setAttr(placeholder + '.parent', parents[0], type="string") + parents = cmds.ls(selected, long=True)""" + cmds.setAttr(placeholder + '.parent', '', type="string") def create_options(args): From a97f5379b16f4141035629aa23d7ca26f16fdced Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:17:30 +0200 Subject: [PATCH 034/122] Add documentation --- website/docs/admin_hosts_maya.md | 50 ++++++++++++++++++ .../docs/assets/maya-create_placeholder.png | Bin 0 -> 31543 bytes website/docs/assets/maya-placeholder_new.png | Bin 0 -> 28008 bytes 3 files changed, 50 insertions(+) create mode 100644 website/docs/assets/maya-create_placeholder.png create mode 100644 website/docs/assets/maya-placeholder_new.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 93bf32798f..c55dcc1b36 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -120,3 +120,53 @@ raw json. You can configure path mapping using Maya `dirmap` command. This will add bi-directional mapping between list of paths specified in **Settings**. You can find it in **Settings -> Project Settings -> Maya -> Maya Directory Mapping** ![Dirmap settings](assets/maya-admin_dirmap_settings.png) + +## Templated Build Workfile + +Building a workfile using a template designed by users. Helping to assert homogeneous subsets hierarchy and imports. Template stored as file easy to define, change and customize for production needs. + + **1. Make a template** + +Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. + +![Dirmap settings](assets/maya-workfile-outliner.png) + +If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** + +![create placeholder](assets/maya-create_placeholder.png) + +- **Configure placeholders** + +Fill in the necessary fields (the optional fields are regex filters) + +![new place holder](assets/maya-placeholder_new.png) + + + - Builder type: Wether the the placeholder should load current asset representations or linked assets representations + + - Representation: Representation that will be loaded (ex: ma, abc, png, etc...) + + - Family: Family of the representation to load (main, look, image, etc ...) + + - Loader: Placeholder loader name that will be used to load corresponding representations + + - Order: Priority for current placeholder loader (priority is lowest first, highet last) + +- **Save your template** + + + **2. Configure Template** + +- **Go to Studio settings > Project > Your DCC > Templated Build Settings** +- Add a profile for your task and enter path to your template +![Dirmap settings](assets/settings/template_build_workfile.png) + +**3. Build your workfile** + +- Open maya + +- Build your workfile + +![Dirmap settings](assets/maya-build_workfile_from_template.png) + + diff --git a/website/docs/assets/maya-create_placeholder.png b/website/docs/assets/maya-create_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..3f49fe2e2b801e89e2027ec521dbf700b97f80d2 GIT binary patch literal 31543 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 Pn$XtJKYU;9ROtTzYS3%b literal 0 HcmV?d00001 diff --git a/website/docs/assets/maya-placeholder_new.png b/website/docs/assets/maya-placeholder_new.png new file mode 100644 index 0000000000000000000000000000000000000000..106a5275cdb5bc0588c811d9a059ec3b6e072e20 GIT binary patch literal 28008 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 literal 0 HcmV?d00001 From e4a7dd6548eec1c3d85bdf133c4f273432313c12 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:27:00 +0200 Subject: [PATCH 035/122] add sreenshot --- .../maya-build_workfile_from_template.png | Bin 0 -> 20676 bytes .../assets/settings/template_build_workfile.png | Bin 0 -> 29814 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 website/docs/assets/maya-build_workfile_from_template.png create mode 100644 website/docs/assets/settings/template_build_workfile.png diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png new file mode 100644 index 0000000000000000000000000000000000000000..336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8 GIT binary patch literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png new file mode 100644 index 0000000000000000000000000000000000000000..7ef87861fe97322a3b23c28ee3644b76ce995e9d GIT binary patch literal 29814 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 literal 0 HcmV?d00001 From 4441a7cc1c07dc62fc04bd7428bd2f7efcdb0fd9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:50:42 +0200 Subject: [PATCH 036/122] add screenshot --- website/docs/admin_hosts_maya.md | 5 +++-- .../maya-build_workfile_from_template.png | Bin 20676 -> 29814 bytes .../docs/assets/maya-workfile-outliner.png | Bin 0 -> 4835 bytes .../settings/template_build_workfile.png | Bin 29814 -> 12596 bytes 4 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 website/docs/assets/maya-workfile-outliner.png diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index c55dcc1b36..0ba030c26f 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -129,7 +129,7 @@ Building a workfile using a template designed by users. Helping to assert homoge Make your template. Add families and everything needed for your tasks. Here is an example template for the modeling task using a placeholder to import a gauge. -![Dirmap settings](assets/maya-workfile-outliner.png) +![maya outliner](assets/maya-workfile-outliner.png) If needed, you can add placeholders when the template needs to load some assets. **OpenPype > Template Builder > Create Placeholder** @@ -159,7 +159,8 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![Dirmap settings](assets/settings/template_build_workfile.png) + +![build template](assets/settings/template_build_workfile.png) **3. Build your workfile** diff --git a/website/docs/assets/maya-build_workfile_from_template.png b/website/docs/assets/maya-build_workfile_from_template.png index 336b76f8aa1ef8d22aa8b912da1cc5f4827fc5e8..7ef87861fe97322a3b23c28ee3644b76ce995e9d 100644 GIT binary patch literal 29814 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 literal 20676 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 Z{|`+)aTxx~sMG)e002ovPDHLkV1iGtk!t_| diff --git a/website/docs/assets/maya-workfile-outliner.png b/website/docs/assets/maya-workfile-outliner.png new file mode 100644 index 0000000000000000000000000000000000000000..fbd1bbd03bce52041ad511dac138479494641361 GIT binary patch literal 4835 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 X`o37wi;`y6#S|Ob%*wRV#3TNnn_AI~ literal 0 HcmV?d00001 diff --git a/website/docs/assets/settings/template_build_workfile.png b/website/docs/assets/settings/template_build_workfile.png index 7ef87861fe97322a3b23c28ee3644b76ce995e9d..1bea5b01f5f3145e2d1aa5d46545c3739aaf2584 100644 GIT binary patch literal 12596 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 literal 29814 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 From 90754fb0b8d36eb0b34cba6da48f672505470f0e Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 14:51:02 +0200 Subject: [PATCH 037/122] modify readme doc --- website/docs/admin_hosts_maya.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/admin_hosts_maya.md b/website/docs/admin_hosts_maya.md index 0ba030c26f..0e77f29fc2 100644 --- a/website/docs/admin_hosts_maya.md +++ b/website/docs/admin_hosts_maya.md @@ -160,7 +160,7 @@ Fill in the necessary fields (the optional fields are regex filters) - **Go to Studio settings > Project > Your DCC > Templated Build Settings** - Add a profile for your task and enter path to your template -![build template](assets/settings/template_build_workfile.png) +![setting build template](assets/settings/template_build_workfile.png) **3. Build your workfile** @@ -168,6 +168,6 @@ Fill in the necessary fields (the optional fields are regex filters) - Build your workfile -![Dirmap settings](assets/maya-build_workfile_from_template.png) +![maya build template](assets/maya-build_workfile_from_template.png) From d3c2dc57d8f39865c3f3900db1be1f911b6436c9 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:02:43 +0200 Subject: [PATCH 038/122] fix linter --- openpype/hosts/maya/api/lib_template_builder.py | 6 +++++- openpype/lib/build_template_exceptions.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index bed4291e3d..e5254b7f87 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,7 +43,11 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls(selection[0], long=True)[0] + '|' + placeholder.replace('|', '') + placeholder_full_name = cmds.ls( + selection[0], + long=True)[0] + '|' + placeholder.replace('|', + '' + ) if selection: cmds.parent(placeholder, selection[0]) diff --git a/openpype/lib/build_template_exceptions.py b/openpype/lib/build_template_exceptions.py index d781eff204..7a5075e3dc 100644 --- a/openpype/lib/build_template_exceptions.py +++ b/openpype/lib/build_template_exceptions.py @@ -32,4 +32,4 @@ class TemplateAlreadyImported(Exception): class TemplateLoadingFailed(Exception): """Error raised whend Template loader was unable to load the template""" - pass \ No newline at end of file + pass From bc9c5b183171b9ef03b88062c7536a5effed3ae1 Mon Sep 17 00:00:00 2001 From: "clement.hector" Date: Fri, 10 Jun 2022 15:22:33 +0200 Subject: [PATCH 039/122] fix linter lengh line --- openpype/hosts/maya/api/lib_template_builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index e5254b7f87..a30b3868b0 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -43,11 +43,8 @@ def create_placeholder(): placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) - placeholder_full_name = cmds.ls( - selection[0], - long=True)[0] + '|' + placeholder.replace('|', - '' - ) + placeholder_full_name = cmds.ls(selection[0], long=True)[ + 0] + '|' + placeholder.replace('|', '') if selection: cmds.parent(placeholder, selection[0]) From ba1abf8b15e1476b430180bc63b1a3801ff118ef Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 10:35:41 +0200 Subject: [PATCH 040/122] add task name field in build templated workfile settings --- openpype/settings/defaults/project_settings/maya.json | 1 + .../schemas/schema_templated_workfile_build.json | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 2e0e30b74b..453706ff88 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -722,6 +722,7 @@ "profiles": [ { "task_types": [], + "tasks": [], "path": "/path/to/your/template" } ] diff --git a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json index 01e74f64b0..a591facf98 100644 --- a/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json +++ b/openpype/settings/entities/schemas/projects_schema/schemas/schema_templated_workfile_build.json @@ -16,6 +16,12 @@ "label": "Task types", "type": "task-types-enum" }, + { + "key": "tasks", + "label": "Task names", + "type": "list", + "object_type": "text" + }, { "key": "path", "label": "Path to template", From ef7627199eb8297196c9d0a778e222132d742be2 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Tue, 14 Jun 2022 11:37:08 +0200 Subject: [PATCH 041/122] add a task name verification for template loader --- openpype/lib/abstract_template_loader.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/lib/abstract_template_loader.py index 159d5c8f6c..e296e3207f 100644 --- a/openpype/lib/abstract_template_loader.py +++ b/openpype/lib/abstract_template_loader.py @@ -160,6 +160,8 @@ class AbstractTemplateLoader: for prf in profiles: if prf['task_types'] and task_type not in prf['task_types']: continue + if prf['tasks'] and task_name not in prf['tasks']: + continue path = prf['path'] break else: # IF no template were found (no break happened) From 01f2c59049be47fce42a5bfdcf67ef1227be1d11 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 15:15:40 +0200 Subject: [PATCH 042/122] the update placeholder keep placeholder info when canceled or closed --- openpype/hosts/maya/api/lib_template_builder.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index a30b3868b0..855c72e361 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -107,11 +107,13 @@ def update_placeholder(): placeholder = placeholder[0] args = placeholder_window(get_placeholder_attributes(placeholder)) - # delete placeholder attributes - delete_placeholder_attributes(placeholder) + if not args: return # operation canceled + # delete placeholder attributes + delete_placeholder_attributes(placeholder) + options = create_options(args) imprint(placeholder, options) From e6cad709cd5904087f687a4e2ec5a15641ab48e0 Mon Sep 17 00:00:00 2001 From: Thomas Fricard Date: Mon, 18 Jul 2022 18:43:34 +0200 Subject: [PATCH 043/122] fix error when updating workfile from template with empty scene --- openpype/hosts/maya/api/template_loader.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 0e346ca411..c7946b6ad3 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -80,7 +80,11 @@ class MayaTemplateLoader(AbstractTemplateLoader): return [attribute.rpartition('.')[0] for attribute in attributes] def get_loaded_containers_by_id(self): - containers = cmds.sets('AVALON_CONTAINERS', q=True) + try: + containers = cmds.sets("AVALON_CONTAINERS", q=True) + except ValueError: + return None + return [ cmds.getAttr(container + '.representation') for container in containers] From b5fb016331e3a86397f6337f44e4c885caf9cff1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:10:08 +0200 Subject: [PATCH 044/122] moved abstract template loader into openpype/pipeline/workfile --- openpype/{lib => pipeline/workfile}/abstract_template_loader.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename openpype/{lib => pipeline/workfile}/abstract_template_loader.py (100%) diff --git a/openpype/lib/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py similarity index 100% rename from openpype/lib/abstract_template_loader.py rename to openpype/pipeline/workfile/abstract_template_loader.py From b1f2831868001431ab5b949cf2a85729a9adfb04 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:12:04 +0200 Subject: [PATCH 045/122] moved 'get_loaders_by_name' to load utils --- openpype/lib/avalon_context.py | 15 --------------- openpype/pipeline/load/__init__.py | 2 ++ openpype/pipeline/load/utils.py | 14 ++++++++++++++ 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 316c8ad67e..86902cac56 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -943,21 +943,6 @@ def collect_last_version_repres(asset_entities): return output -@with_pipeline_io -def get_loaders_by_name(): - from openpype.pipeline import discover_loader_plugins - - loaders_by_name = {} - for loader in discover_loader_plugins(): - loader_name = loader.__name__ - if loader_name in loaders_by_name: - raise KeyError( - "Duplicated loader name {} !".format(loader_name) - ) - loaders_by_name[loader_name] = loader - return loaders_by_name - - class BuildWorkfile: """Wrapper for build workfile process. diff --git a/openpype/pipeline/load/__init__.py b/openpype/pipeline/load/__init__.py index e46d9f152b..b6bdd13d50 100644 --- a/openpype/pipeline/load/__init__.py +++ b/openpype/pipeline/load/__init__.py @@ -16,6 +16,7 @@ from .utils import ( switch_container, get_loader_identifier, + get_loaders_by_name, get_representation_path_from_context, get_representation_path, @@ -61,6 +62,7 @@ __all__ = ( "switch_container", "get_loader_identifier", + "get_loaders_by_name", "get_representation_path_from_context", "get_representation_path", diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index fe5102353d..9945e1fce4 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -369,6 +369,20 @@ def get_loader_identifier(loader): return loader.__name__ +def get_loaders_by_name(): + from .plugins import discover_loader_plugins + + loaders_by_name = {} + for loader in discover_loader_plugins(): + loader_name = loader.__name__ + if loader_name in loaders_by_name: + raise KeyError( + "Duplicated loader name {} !".format(loader_name) + ) + loaders_by_name[loader_name] = loader + return loaders_by_name + + def _get_container_loader(container): """Return the Loader corresponding to the container""" from .plugins import discover_loader_plugins From b2b6ffe0e4290840fc1ca1b5c98174f2bdfcbfaf Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:13:56 +0200 Subject: [PATCH 046/122] updated 'collect_last_version_repres' with latest develop --- openpype/lib/avalon_context.py | 68 +++++++++++++++------------------- 1 file changed, 30 insertions(+), 38 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 86902cac56..4b552d13ed 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -847,7 +847,7 @@ def save_workfile_data_to_doc(workfile_doc, data, dbcon=None): @with_pipeline_io -def collect_last_version_repres(asset_entities): +def collect_last_version_repres(asset_docs): """Collect subsets, versions and representations for asset_entities. Args: @@ -880,64 +880,56 @@ def collect_last_version_repres(asset_entities): ``` """ - if not asset_entities: - return {} + output = {} + if not asset_docs: + return output - asset_entity_by_ids = {asset["_id"]: asset for asset in asset_entities} + asset_docs_by_ids = {asset["_id"]: asset for asset in asset_docs} - subsets = list(legacy_io.find({ - "type": "subset", - "parent": {"$in": list(asset_entity_by_ids.keys())} - })) + project_name = legacy_io.active_project() + subsets = list(get_subsets( + project_name, asset_ids=asset_docs_by_ids.keys() + )) subset_entity_by_ids = {subset["_id"]: subset for subset in subsets} - sorted_versions = list(legacy_io.find({ - "type": "version", - "parent": {"$in": list(subset_entity_by_ids.keys())} - }).sort("name", -1)) + last_version_by_subset_id = get_last_versions( + project_name, subset_entity_by_ids.keys() + ) + last_version_docs_by_id = { + version["_id"]: version + for version in last_version_by_subset_id.values() + } + repre_docs = get_representations( + project_name, version_ids=last_version_docs_by_id.keys() + ) - subset_id_with_latest_version = [] - last_versions_by_id = {} - for version in sorted_versions: - subset_id = version["parent"] - if subset_id in subset_id_with_latest_version: - continue - subset_id_with_latest_version.append(subset_id) - last_versions_by_id[version["_id"]] = version + for repre_doc in repre_docs: + version_id = repre_doc["parent"] + version_doc = last_version_docs_by_id[version_id] - repres = legacy_io.find({ - "type": "representation", - "parent": {"$in": list(last_versions_by_id.keys())} - }) + subset_id = version_doc["parent"] + subset_doc = subset_entity_by_ids[subset_id] - output = {} - for repre in repres: - version_id = repre["parent"] - version = last_versions_by_id[version_id] - - subset_id = version["parent"] - subset = subset_entity_by_ids[subset_id] - - asset_id = subset["parent"] - asset = asset_entity_by_ids[asset_id] + asset_id = subset_doc["parent"] + asset_doc = asset_docs_by_ids[asset_id] if asset_id not in output: output[asset_id] = { - "asset_entity": asset, + "asset_entity": asset_doc, "subsets": {} } if subset_id not in output[asset_id]["subsets"]: output[asset_id]["subsets"][subset_id] = { - "subset_entity": subset, + "subset_entity": subset_doc, "version": { - "version_entity": version, + "version_entity": version_doc, "repres": [] } } output[asset_id]["subsets"][subset_id]["version"]["repres"].append( - repre + repre_doc ) return output From 9b4b44ef3bdf490fca2a4df0f3451143a09e555c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:17:26 +0200 Subject: [PATCH 047/122] moved build template code into workfile --- openpype/{lib => pipeline/workfile}/build_template.py | 0 openpype/{lib => pipeline/workfile}/build_template_exceptions.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename openpype/{lib => pipeline/workfile}/build_template.py (100%) rename openpype/{lib => pipeline/workfile}/build_template_exceptions.py (100%) diff --git a/openpype/lib/build_template.py b/openpype/pipeline/workfile/build_template.py similarity index 100% rename from openpype/lib/build_template.py rename to openpype/pipeline/workfile/build_template.py diff --git a/openpype/lib/build_template_exceptions.py b/openpype/pipeline/workfile/build_template_exceptions.py similarity index 100% rename from openpype/lib/build_template_exceptions.py rename to openpype/pipeline/workfile/build_template_exceptions.py From 6462bf15d04ad53eaed484069e70f2c2312f0a2f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:24:16 +0200 Subject: [PATCH 048/122] fixed imports --- .../workfile/abstract_template_loader.py | 24 ++++++++++--------- openpype/pipeline/workfile/build_template.py | 4 ++-- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e296e3207f..e95b89b518 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -4,23 +4,25 @@ from abc import ABCMeta, abstractmethod import traceback import six - -from openpype.settings import get_project_settings -from openpype.lib import Anatomy, get_linked_assets, get_loaders_by_name -from openpype.api import PypeLogger as Logger -from openpype.pipeline import legacy_io, load - +import logging from functools import reduce -from openpype.lib.build_template_exceptions import ( +from openpype.settings import get_project_settings +from openpype.lib import get_linked_assets, PypeLogger as Logger +from openpype.pipeline import legacy_io, Anatomy +from openpype.pipeline.load import ( + get_loaders_by_name, + get_representation_context, + load_with_repre_context, +) + +from .build_template_exceptions import ( TemplateAlreadyImported, TemplateLoadingFailed, TemplateProfileNotFound, TemplateNotFound ) -import logging - log = logging.getLogger(__name__) @@ -289,8 +291,8 @@ class AbstractTemplateLoader: pass def load(self, placeholder, loaders_by_name, last_representation): - repre = load.get_representation_context(last_representation) - return load.load_with_repre_context( + repre = get_representation_context(last_representation) + return load_with_repre_context( loaders_by_name[placeholder.loader], repre, options=parse_loader_args(placeholder.data['loader_args'])) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index 7f749cbec2..f4b57218fb 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -1,6 +1,6 @@ -from openpype.pipeline import registered_host -from openpype.lib import classes_from_module from importlib import import_module +from openpype.lib import classes_from_module +from openpype.pipeline import registered_host from .abstract_template_loader import ( AbstractPlaceholder, From 5dfb12a217f24e5551ec3f4a982823254efdb00e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:24:44 +0200 Subject: [PATCH 049/122] logger is created dynamically on demand and is using class name --- openpype/pipeline/workfile/abstract_template_loader.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e95b89b518..27823479cf 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -72,6 +72,7 @@ class AbstractTemplateLoader: """ def __init__(self, placeholder_class): + self._log = None self.loaders_by_name = get_loaders_by_name() self.current_asset = legacy_io.Session["AVALON_ASSET"] @@ -91,8 +92,6 @@ class AbstractTemplateLoader: .get("type") ) - self.log = Logger().get_logger("BUILD TEMPLATE") - self.log.info( "BUILDING ASSET FROM TEMPLATE :\n" "Starting templated build for {asset} in {project}\n\n" @@ -112,6 +111,12 @@ class AbstractTemplateLoader: "There is no registered loaders. No assets will be loaded") return + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(self.__class__.__name__) + return self._log + def template_already_imported(self, err_msg): """In case template was already loaded. Raise the error as a default action. From 764207d033fc049f6726f901a99732c928595768 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:25:04 +0200 Subject: [PATCH 050/122] fix missing import 'get_loaders_by_name' --- openpype/lib/avalon_context.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 4b552d13ed..e60dbb9e8f 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -992,6 +992,9 @@ class BuildWorkfile: ... }] """ + + from openpype.pipeline.load import get_loaders_by_name + # Get current asset name and entity project_name = legacy_io.active_project() current_asset_name = legacy_io.Session["AVALON_ASSET"] From fe38df50bff954993570cd113371044dde4a5e43 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:26:18 +0200 Subject: [PATCH 051/122] removed 'get_loaders_by_name' from openpype lib init file --- openpype/lib/__init__.py | 2 -- openpype/pipeline/workfile/__init__.py | 0 2 files changed, 2 deletions(-) create mode 100644 openpype/pipeline/workfile/__init__.py diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index f4efffd726..fb52a9aca7 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -135,7 +135,6 @@ from .avalon_context import ( create_workfile_doc, save_workfile_data_to_doc, get_workfile_doc, - get_loaders_by_name, BuildWorkfile, @@ -307,7 +306,6 @@ __all__ = [ "create_workfile_doc", "save_workfile_data_to_doc", "get_workfile_doc", - "get_loaders_by_name", "BuildWorkfile", diff --git a/openpype/pipeline/workfile/__init__.py b/openpype/pipeline/workfile/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From c9ac330e2ebedc6e9900e0d2e6207a20326d0139 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:31:18 +0200 Subject: [PATCH 052/122] fixed imports in maya --- openpype/hosts/maya/api/menu.py | 6 +++--- openpype/hosts/maya/api/template_loader.py | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/maya/api/menu.py b/openpype/hosts/maya/api/menu.py index c0bad7092f..833fbae881 100644 --- a/openpype/hosts/maya/api/menu.py +++ b/openpype/hosts/maya/api/menu.py @@ -8,12 +8,12 @@ import maya.cmds as cmds from openpype.api import BuildWorkfile -from openpype.lib.build_template import ( +from openpype.settings import get_project_settings +from openpype.pipeline import legacy_io +from openpype.pipeline.workfile.build_template import ( build_workfile_template, update_workfile_template ) -from openpype.settings import get_project_settings -from openpype.pipeline import legacy_io from openpype.tools.utils import host_tools from openpype.hosts.maya.api import lib diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index c7946b6ad3..6b225442e7 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -1,11 +1,13 @@ from maya import cmds from openpype.pipeline import legacy_io -from openpype.lib.abstract_template_loader import ( +from openpype.pipeline.workfile.abstract_template_loader import ( AbstractPlaceholder, AbstractTemplateLoader ) -from openpype.lib.build_template_exceptions import TemplateAlreadyImported +from openpype.pipeline.workfile.build_template_exceptions import ( + TemplateAlreadyImported +) PLACEHOLDER_SET = 'PLACEHOLDERS_SET' From 1e8cf2a6ea87ded1131d5d3012cdd5980dc2f183 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 27 Jul 2022 10:36:04 +0200 Subject: [PATCH 053/122] make sure '_log' attribute is available before abc init --- openpype/pipeline/workfile/abstract_template_loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 27823479cf..3d942a0bdd 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -71,9 +71,9 @@ class AbstractTemplateLoader: as placeholders. Depending on current host """ - def __init__(self, placeholder_class): - self._log = None + _log = None + def __init__(self, placeholder_class): self.loaders_by_name = get_loaders_by_name() self.current_asset = legacy_io.Session["AVALON_ASSET"] self.project_name = legacy_io.Session["AVALON_PROJECT"] From 6bb28d16df22e4d5c4cf6e763a85a545ba6da833 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 28 Jul 2022 11:53:51 +0200 Subject: [PATCH 054/122] fix build template and added few comments --- .../workfile/abstract_template_loader.py | 30 +++++++++++++------ openpype/pipeline/workfile/build_template.py | 9 +++++- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 3d942a0bdd..00bc8f15a7 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -7,6 +7,7 @@ import six import logging from functools import reduce +from openpype.client import get_asset_by_name from openpype.settings import get_project_settings from openpype.lib import get_linked_assets, PypeLogger as Logger from openpype.pipeline import legacy_io, Anatomy @@ -74,18 +75,25 @@ class AbstractTemplateLoader: _log = None def __init__(self, placeholder_class): + # TODO template loader should expect host as and argument + # - host have all responsibility for most of code (also provide + # placeholder class) + # - also have responsibility for current context + # - this won't work in DCCs where multiple workfiles with + # different contexts can be opened at single time + # - template loader should have ability to change context + project_name = legacy_io.active_project() + asset_name = legacy_io.Session["AVALON_ASSET"] + self.loaders_by_name = get_loaders_by_name() - self.current_asset = legacy_io.Session["AVALON_ASSET"] - self.project_name = legacy_io.Session["AVALON_PROJECT"] + self.current_asset = asset_name + self.project_name = project_name self.host_name = legacy_io.Session["AVALON_APP"] self.task_name = legacy_io.Session["AVALON_TASK"] self.placeholder_class = placeholder_class - self.current_asset_docs = legacy_io.find_one({ - "type": "asset", - "name": self.current_asset - }) + self.current_asset_doc = get_asset_by_name(project_name, asset_name) self.task_type = ( - self.current_asset_docs + self.current_asset_doc .get("data", {}) .get("tasks", {}) .get(self.task_name, {}) @@ -218,7 +226,7 @@ class AbstractTemplateLoader: loaders_by_name = self.loaders_by_name current_asset = self.current_asset linked_assets = [asset['name'] for asset - in get_linked_assets(self.current_asset_docs)] + in get_linked_assets(self.current_asset_doc)] ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() @@ -270,7 +278,11 @@ class AbstractTemplateLoader: self.postload(placeholder) def get_placeholder_representations( - self, placeholder, current_asset, linked_assets): + self, placeholder, current_asset, linked_assets + ): + # TODO This approach must be changed. Placeholders should return + # already prepared data and not query them here. + # - this is impossible to handle using query functions placeholder_db_filters = placeholder.convert_to_db_filters( current_asset, linked_assets) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index f4b57218fb..df6fe3514a 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -1,5 +1,6 @@ from importlib import import_module from openpype.lib import classes_from_module +from openpype.host import HostBase from openpype.pipeline import registered_host from .abstract_template_loader import ( @@ -35,7 +36,13 @@ def update_workfile_template(args): def build_template_loader(): - host_name = registered_host().__name__.partition('.')[2] + # TODO refactor to use advantage of 'HostBase' and don't import dynamically + # - hosts should have methods that gives option to return builders + host = registered_host() + if isinstance(host, HostBase): + host_name = host.name + else: + host_name = host.__name__.partition('.')[2] module_path = _module_path_format.format(host=host_name) module = import_module(module_path) if not module: From 2d601d051a9b59509c6af159c06f8424591af444 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 16:42:54 +0200 Subject: [PATCH 055/122] give ability to query by representation context and regex --- openpype/client/entities.py | 108 +++++++++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index dd5d831ecf..57c38784b0 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -7,6 +7,7 @@ that has project name as a context (e.g. on 'ProjectEntity'?). """ import os +import re import collections import six @@ -1035,17 +1036,70 @@ def get_representation_by_name( return conn.find_one(query_filter, _prepare_fields(fields)) +def _flatten_dict(data): + flatten_queue = collections.deque() + flatten_queue.append(data) + output = {} + while flatten_queue: + item = flatten_queue.popleft() + for key, value in item.items(): + if not isinstance(value, dict): + output[key] = value + continue + + tmp = {} + for subkey, subvalue in value.items(): + new_key = "{}.{}".format(key, subkey) + tmp[new_key] = subvalue + flatten_queue.append(tmp) + return output + + +def _regex_filters(filters): + output = [] + for key, value in filters.items(): + regexes = [] + a_values = [] + if isinstance(value, re.Pattern): + regexes.append(value) + elif isinstance(value, (list, tuple, set)): + for item in value: + if isinstance(item, re.Pattern): + regexes.append(item) + else: + a_values.append(item) + else: + a_values.append(value) + + key_filters = [] + if len(a_values) == 1: + key_filters.append({key: a_values[0]}) + elif a_values: + key_filters.append({key: {"$in": a_values}}) + + for regex in regexes: + key_filters.append({key: {"$regex": regex}}) + + if len(key_filters) == 1: + output.append(key_filters[0]) + else: + output.append({"$or": key_filters}) + + return output + + def _get_representations( project_name, representation_ids, representation_names, version_ids, - extensions, + context_filters, names_by_version_ids, standard, archived, fields ): + default_output = [] repre_types = [] if standard: repre_types.append("representation") @@ -1053,7 +1107,7 @@ def _get_representations( repre_types.append("archived_representation") if not repre_types: - return [] + return default_output if len(repre_types) == 1: query_filter = {"type": repre_types[0]} @@ -1063,25 +1117,21 @@ def _get_representations( if representation_ids is not None: representation_ids = _convert_ids(representation_ids) if not representation_ids: - return [] + return default_output query_filter["_id"] = {"$in": representation_ids} if representation_names is not None: if not representation_names: - return [] + return default_output query_filter["name"] = {"$in": list(representation_names)} if version_ids is not None: version_ids = _convert_ids(version_ids) if not version_ids: - return [] + return default_output query_filter["parent"] = {"$in": version_ids} - if extensions is not None: - if not extensions: - return [] - query_filter["context.ext"] = {"$in": list(extensions)} - + or_queries = [] if names_by_version_ids is not None: or_query = [] for version_id, names in names_by_version_ids.items(): @@ -1091,8 +1141,35 @@ def _get_representations( "name": {"$in": list(names)} }) if not or_query: + return default_output + or_queries.append(or_query) + + if context_filters is not None: + if not context_filters: return [] - query_filter["$or"] = or_query + _flatten_filters = _flatten_dict(context_filters) + flatten_filters = {} + for key, value in _flatten_filters.items(): + if not key.startswith("context"): + key = "context.{}".format(key) + flatten_filters[key] = value + + for item in _regex_filters(flatten_filters): + for key, value in item.items(): + if key == "$or": + or_queries.append(value) + else: + query_filter[key] = value + + if len(or_queries) == 1: + query_filter["$or"] = or_queries[0] + elif or_queries: + and_query = [] + for or_query in or_queries: + if isinstance(or_query, list): + or_query = {"$or": or_query} + and_query.append(or_query) + query_filter["$and"] = and_query conn = get_project_connection(project_name) @@ -1104,7 +1181,7 @@ def get_representations( representation_ids=None, representation_names=None, version_ids=None, - extensions=None, + context_filters=None, names_by_version_ids=None, archived=False, standard=True, @@ -1122,8 +1199,8 @@ def get_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. - extensions (Iterable[str]): Filter by extension of main representation - file (without dot). + context_filters (Dict[str, List[str, re.Pattern]]): Filter by + representation context fields. names_by_version_ids (dict[ObjectId, list[str]]): Complex filtering using version ids and list of names under the version. archived (bool): Output will also contain archived representations. @@ -1140,6 +1217,7 @@ def get_representations( representation_names=representation_names, version_ids=version_ids, extensions=extensions, + context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=True, archived=archived, @@ -1153,6 +1231,7 @@ def get_archived_representations( representation_names=None, version_ids=None, extensions=None, + context_filters=None, names_by_version_ids=None, fields=None ): @@ -1185,6 +1264,7 @@ def get_archived_representations( representation_names=representation_names, version_ids=version_ids, extensions=extensions, + context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=False, archived=True, From c65dd9747f5197868a9153fc109915ed654122ab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:15:13 +0200 Subject: [PATCH 056/122] added new method 'get_representations' to get representations from placeholder --- .../workfile/abstract_template_loader.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 00bc8f15a7..0a422f5cca 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -456,8 +456,25 @@ class AbstractPlaceholder: @abstractmethod def clean(self): - """Clean placeholder from hierarchy after loading assets. + """Clean placeholder from hierarchy after loading assets.""" + + pass + + @abstractmethod + def get_representations(self, current_asset, linked_assets): + """Query representations based on placeholder data. + + Args: + current_asset (str): Name of current + context asset. + linked_assets (List[str]): Names of assets + linked to current context asset. + + Returns: + Iterable[Dict[str, Any]]: Representations that are matching + placeholder filters. """ + pass @abstractmethod From da8e25f4a1b7ea89bf9c7cac62c8a3ea10fbb9e6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:16:23 +0200 Subject: [PATCH 057/122] use 'get_representations' instead of 'convert_to_db_filters' --- .../workfile/abstract_template_loader.py | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 0a422f5cca..a2505c061e 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -280,19 +280,16 @@ class AbstractTemplateLoader: def get_placeholder_representations( self, placeholder, current_asset, linked_assets ): - # TODO This approach must be changed. Placeholders should return - # already prepared data and not query them here. - # - this is impossible to handle using query functions - placeholder_db_filters = placeholder.convert_to_db_filters( + placeholder_representations = placeholder.get_representations( current_asset, - linked_assets) - # get representation by assets - for db_filter in placeholder_db_filters: - placeholder_representations = list(legacy_io.find(db_filter)) - for representation in reduce(update_representations, - placeholder_representations, - dict()).values(): - yield representation + linked_assets + ) + for repre_doc in reduce( + update_representations, + placeholder_representations, + dict() + ).values(): + yield repre_doc def load_data_is_incorrect( self, placeholder, last_representation, ignored_ids): From 0e0cec5e0146a3001a4a349360324346fd0ab961 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:19:47 +0200 Subject: [PATCH 058/122] pass asset documents instead of just names --- .../workfile/abstract_template_loader.py | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a2505c061e..96012eba36 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -223,10 +223,10 @@ class AbstractTemplateLoader: Returns: None """ + loaders_by_name = self.loaders_by_name - current_asset = self.current_asset - linked_assets = [asset['name'] for asset - in get_linked_assets(self.current_asset_doc)] + current_asset_doc = self.current_asset_doc + linked_assets = get_linked_assets(current_asset_doc) ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() @@ -239,7 +239,7 @@ class AbstractTemplateLoader: )) placeholder_representations = self.get_placeholder_representations( placeholder, - current_asset, + current_asset_doc, linked_assets ) @@ -278,11 +278,11 @@ class AbstractTemplateLoader: self.postload(placeholder) def get_placeholder_representations( - self, placeholder, current_asset, linked_assets + self, placeholder, current_asset_doc, linked_asset_docs ): placeholder_representations = placeholder.get_representations( - current_asset, - linked_assets + current_asset_doc, + linked_asset_docs ) for repre_doc in reduce( update_representations, @@ -458,13 +458,13 @@ class AbstractPlaceholder: pass @abstractmethod - def get_representations(self, current_asset, linked_assets): + def get_representations(self, current_asset_doc, linked_asset_docs): """Query representations based on placeholder data. Args: - current_asset (str): Name of current + current_asset_doc (Dict[str, Any]): Document of current context asset. - linked_assets (List[str]): Names of assets + linked_asset_docs (List[Dict[str, Any]]): Documents of assets linked to current context asset. Returns: From ef674857f85f360954b4d6e2c6f6c0c4acf3f711 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:29:34 +0200 Subject: [PATCH 059/122] implemented get_representations for maya placeholder --- openpype/hosts/maya/api/template_loader.py | 80 +++++++++++----------- 1 file changed, 41 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index 6b225442e7..f553730186 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -1,5 +1,7 @@ +import re from maya import cmds +from openpype.client import get_representations from openpype.pipeline import legacy_io from openpype.pipeline.workfile.abstract_template_loader import ( AbstractPlaceholder, @@ -191,48 +193,48 @@ class MayaPlaceholder(AbstractPlaceholder): cmds.hide(node) cmds.setAttr(node + '.hiddenInOutliner', True) - def convert_to_db_filters(self, current_asset, linked_asset): - if self.data['builder_type'] == "context_asset": - return [ - { - "type": "representation", - "context.asset": { - "$eq": current_asset, - "$regex": self.data['asset'] - }, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } - ] + def get_representations(self, current_asset_doc, linked_asset_docs): + project_name = legacy_io.active_project() - elif self.data['builder_type'] == "linked_asset": - return [ - { - "type": "representation", - "context.asset": { - "$eq": asset_name, - "$regex": self.data['asset'] - }, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } for asset_name in linked_asset - ] + builder_type = self.data["builder_type"] + if builder_type == "context_asset": + context_filters = { + "asset": [current_asset_doc["name"]], + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representations": [self.data["representation"]], + "family": [self.data["family"]] + } + + elif builder_type != "linked_asset": + context_filters = { + "asset": [re.compile(self.data["asset"])], + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representation": [self.data["representation"]], + "family": [self.data["family"]] + } else: - return [ - { - "type": "representation", - "context.asset": {"$regex": self.data['asset']}, - "context.subset": {"$regex": self.data['subset']}, - "context.hierarchy": {"$regex": self.data['hierarchy']}, - "context.representation": self.data['representation'], - "context.family": self.data['family'], - } - ] + asset_regex = re.compile(self.data["asset"]) + linked_asset_names = [] + for asset_doc in linked_asset_docs: + asset_name = asset_doc["name"] + if asset_regex.match(asset_name): + linked_asset_names.append(asset_name) + + context_filters = { + "asset": linked_asset_names, + "subset": [re.compile(self.data["subset"])], + "hierarchy": [re.compile(self.data["hierarchy"])], + "representation": [self.data["representation"]], + "family": [self.data["family"]], + } + + return list(get_representations( + project_name, + context_filters=context_filters + )) def err_message(self): return ( From a6406f72d36d8eb748404af8fc6e6d61c6c6b451 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:39:57 +0200 Subject: [PATCH 060/122] added logger to placeholder --- .../pipeline/workfile/abstract_template_loader.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 96012eba36..d934c50daf 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -397,8 +397,19 @@ class AbstractPlaceholder: optional_attributes = {} def __init__(self, node): + self._log = None self.get_data(node) + @property + def log(self): + if self._log is None: + self._log = Logger.get_logger(repr(self)) + return self._log + + def __repr__(self): + return "< {} {} >".format(self.__class__.__name__, self.name) + + def order(self): """Get placeholder order. Order is used to sort them by priority @@ -436,9 +447,9 @@ class AbstractPlaceholder: Bool: True if every attributes are a key of data """ if set(self.attributes).issubset(self.data.keys()): - print("Valid placeholder : {}".format(self.data["node"])) + self.log.debug("Valid placeholder: {}".format(self.data["node"])) return True - print("Placeholder is not valid : {}".format(self.data["node"])) + self.log.info("Placeholder is not valid: {}".format(self.data["node"])) return False @abstractmethod From 8b7531b97775d3facefde41682dd19e9dd3e11f6 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:44:03 +0200 Subject: [PATCH 061/122] added helper attributes to placeholder so there is no need to access it's 'data' --- .../workfile/abstract_template_loader.py | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index d934c50daf..5ecc154ea4 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -231,11 +231,11 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() self.log.debug("Placeholders found in template: {}".format( - [placeholder.data['node'] for placeholder in placeholders] + [placeholder.name] for placeholder in placeholders] )) for placeholder in placeholders: self.log.debug("Start to processing placeholder {}".format( - placeholder.data['node'] + placeholder.name )) placeholder_representations = self.get_placeholder_representations( placeholder, @@ -246,7 +246,7 @@ class AbstractTemplateLoader: if not placeholder_representations: self.log.info( "There's no representation for this placeholder: " - "{}".format(placeholder.data['node']) + "{}".format(placeholder.name) ) continue @@ -264,8 +264,8 @@ class AbstractTemplateLoader: "Loader arguments used : {}".format( representation['context']['asset'], representation['context']['subset'], - placeholder.loader, - placeholder.data['loader_args'])) + placeholder.loader_name, + placeholder.loader_args)) try: container = self.load( @@ -307,19 +307,22 @@ class AbstractTemplateLoader: def load(self, placeholder, loaders_by_name, last_representation): repre = get_representation_context(last_representation) return load_with_repre_context( - loaders_by_name[placeholder.loader], + loaders_by_name[placeholder.loader_name], repre, - options=parse_loader_args(placeholder.data['loader_args'])) + options=parse_loader_args(placeholder.loader_args)) def load_succeed(self, placeholder, container): placeholder.parent_in_hierarchy(container) def load_failed(self, placeholder, last_representation): - self.log.warning("Got error trying to load {}:{} with {}\n\n" - "{}".format(last_representation['context']['asset'], - last_representation['context']['subset'], - placeholder.loader, - traceback.format_exc())) + self.log.warning( + "Got error trying to load {}:{} with {}".format( + last_representation['context']['asset'], + last_representation['context']['subset'], + placeholder.loader_name + ), + exc_info=True + ) def postload(self, placeholder): placeholder.clean() @@ -398,6 +401,7 @@ class AbstractPlaceholder: def __init__(self, node): self._log = None + self._name = node self.get_data(node) @property @@ -409,6 +413,17 @@ class AbstractPlaceholder: def __repr__(self): return "< {} {} >".format(self.__class__.__name__, self.name) + @property + def name(self): + return self._name + + @property + def loader_args(self): + return self.data["loader_args"] + + @property + def builder_type(self): + return self.data["builder_type"] def order(self): """Get placeholder order. @@ -423,12 +438,15 @@ class AbstractPlaceholder: return self.data.get('order') @property - def loader(self): - """Return placeholder loader type + def loader_name(self): + """Return placeholder loader type. + Returns: - string: Loader name + str: Loader name that will be used to load placeholder + representations. """ - return self.data.get('loader') + + return self.data["loader"] @property def is_context(self): From 2d7910a26410936f1d23282b9011780cccfc8680 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:46:14 +0200 Subject: [PATCH 062/122] renamed 'attributes' to 'required_keys' and 'optional_attributes' to 'optional_keys' --- openpype/hosts/maya/api/template_loader.py | 8 +++-- .../workfile/abstract_template_loader.py | 35 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/maya/api/template_loader.py b/openpype/hosts/maya/api/template_loader.py index f553730186..ecffafc93d 100644 --- a/openpype/hosts/maya/api/template_loader.py +++ b/openpype/hosts/maya/api/template_loader.py @@ -98,11 +98,11 @@ class MayaPlaceholder(AbstractPlaceholder): """Concrete implementation of AbstractPlaceholder for maya """ - optional_attributes = {'asset', 'subset', 'hierarchy'} + optional_keys = {'asset', 'subset', 'hierarchy'} def get_data(self, node): user_data = dict() - for attr in self.attributes.union(self.optional_attributes): + for attr in self.required_keys.union(self.optional_keys): attribute_name = '{}.{}'.format(node, attr) if not cmds.attributeQuery(attr, node=node, exists=True): print("{} not found".format(attribute_name)) @@ -112,7 +112,9 @@ class MayaPlaceholder(AbstractPlaceholder): asString=True) user_data['parent'] = ( cmds.getAttr(node + '.parent', asString=True) - or node.rpartition('|')[0] or "") + or node.rpartition('|')[0] + or "" + ) user_data['node'] = node if user_data['parent']: siblings = cmds.listRelatives(user_data['parent'], children=True) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5ecc154ea4..56fb31fa0c 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -377,15 +377,17 @@ class AbstractTemplateLoader: @six.add_metaclass(ABCMeta) class AbstractPlaceholder: - """Abstraction of placeholders logic + """Abstraction of placeholders logic. + Properties: - attributes: A list of mandatory attribute to decribe placeholder + required_keys: A list of mandatory keys to decribe placeholder and assets to load. - optional_attributes: A list of optional attribute to decribe + optional_keys: A list of optional keys to decribe placeholder and assets to load loader: Name of linked loader to use while loading assets is_context: Is placeholder linked to context asset (or to linked assets) + Methods: is_repres_valid: loader: @@ -395,9 +397,15 @@ class AbstractPlaceholder: parent_in_hierachy: """ - attributes = {'builder_type', 'family', 'representation', - 'order', 'loader', 'loader_args'} - optional_attributes = {} + required_keys = { + "builder_type", + "family", + "representation", + "order", + "loader", + "loader_args" + } + optional_keys = {} def __init__(self, node): self._log = None @@ -459,15 +467,18 @@ class AbstractPlaceholder: return self.data.get('builder_type') == 'context_asset' def is_valid(self): - """Test validity of placeholder - i.e.: every attributes exists in placeholder data + """Test validity of placeholder. + + i.e.: every required key exists in placeholder data + Returns: - Bool: True if every attributes are a key of data + bool: True if every key is in data """ - if set(self.attributes).issubset(self.data.keys()): - self.log.debug("Valid placeholder: {}".format(self.data["node"])) + + if set(self.required_keys).issubset(self.data.keys()): + self.log.debug("Valid placeholder : {}".format(self.name)) return True - self.log.info("Placeholder is not valid: {}".format(self.data["node"])) + self.log.info("Placeholder is not valid : {}".format(self.name)) return False @abstractmethod From 736123d1c2496df1604d1b0c84df5a2646cc51f9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 29 Jul 2022 17:48:47 +0200 Subject: [PATCH 063/122] modified 'is_context' property --- .../pipeline/workfile/abstract_template_loader.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 56fb31fa0c..a1d188ea6c 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -458,14 +458,22 @@ class AbstractPlaceholder: @property def is_context(self): - """Return placeholder type + """Check if is placeholder context type. + context_asset: For loading current asset linked_asset: For loading linked assets + + Question: + There seems to be more build options and this property is not used, + should be removed? + Returns: bool: true if placeholder is a context placeholder """ - return self.data.get('builder_type') == 'context_asset' + return self.builder_type == "context_asset" + + @property def is_valid(self): """Test validity of placeholder. From 49799c2d8871a52fb1fd8210b31a1e51fd5f3f2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 4 Aug 2022 18:26:38 +0200 Subject: [PATCH 064/122] fix merge conflict --- openpype/pipeline/workfile/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a1d188ea6c..5d8d79397a 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -231,7 +231,7 @@ class AbstractTemplateLoader: ignored_ids = ignored_ids or [] placeholders = self.get_placeholders() self.log.debug("Placeholders found in template: {}".format( - [placeholder.name] for placeholder in placeholders] + [placeholder.name for placeholder in placeholders] )) for placeholder in placeholders: self.log.debug("Start to processing placeholder {}".format( From 7d1f1bb064190873beee61c0a4eb4df598747c88 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 5 Aug 2022 09:50:11 +0200 Subject: [PATCH 065/122] remove extensions arguments --- openpype/client/entities.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index 57c38784b0..a3fcd01f80 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1216,7 +1216,6 @@ def get_representations( representation_ids=representation_ids, representation_names=representation_names, version_ids=version_ids, - extensions=extensions, context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=True, @@ -1230,7 +1229,6 @@ def get_archived_representations( representation_ids=None, representation_names=None, version_ids=None, - extensions=None, context_filters=None, names_by_version_ids=None, fields=None @@ -1247,8 +1245,6 @@ def get_archived_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. - extensions (Iterable[str]): Filter by extension of main representation - file (without dot). names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. fields (Iterable[str]): Fields that should be returned. All fields are @@ -1263,7 +1259,6 @@ def get_archived_representations( representation_ids=representation_ids, representation_names=representation_names, version_ids=version_ids, - extensions=extensions, context_filters=context_filters, names_by_version_ids=names_by_version_ids, standard=False, From 8db8ada9642bcdf2c5f364fbf78c902344b1613e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:46:32 +0200 Subject: [PATCH 066/122] changed 'node' variable to 'identifier' and added it's docstrings --- .../workfile/abstract_template_loader.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5d8d79397a..16287bbd4e 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -384,17 +384,11 @@ class AbstractPlaceholder: and assets to load. optional_keys: A list of optional keys to decribe placeholder and assets to load - loader: Name of linked loader to use while loading assets - is_context: Is placeholder linked - to context asset (or to linked assets) + loader_name: Name of linked loader to use while loading assets - Methods: - is_repres_valid: - loader: - order: - is_valid: - get_data: - parent_in_hierachy: + Args: + identifier (str): Placeholder identifier. Should be possible to be + used as identifier in "a scene" (e.g. unique node name). """ required_keys = { @@ -407,10 +401,10 @@ class AbstractPlaceholder: } optional_keys = {} - def __init__(self, node): + def __init__(self, identifier): self._log = None - self._name = node - self.get_data(node) + self._name = identifier + self.get_data(identifier) @property def log(self): From 5d0cd42a8133bcf7d65bbcef0c7b093ef058d7b2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:47:01 +0200 Subject: [PATCH 067/122] renamed 'order' method to 'get_order' --- .../pipeline/workfile/abstract_template_loader.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 16287bbd4e..fe1f15c140 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -336,7 +336,7 @@ class AbstractTemplateLoader: placeholders = map(placeholder_class, self.get_template_nodes()) valid_placeholders = filter(placeholder_class.is_valid, placeholders) sorted_placeholders = sorted(valid_placeholders, - key=placeholder_class.order) + key=placeholder_class.get_order) return sorted_placeholders @abstractmethod @@ -427,17 +427,24 @@ class AbstractPlaceholder: def builder_type(self): return self.data["builder_type"] + @property def order(self): - """Get placeholder order. + return self.data["order"] + + def get_order(self): + """Placeholder order. + Order is used to sort them by priority Priority is lowset first, highest last (ex: 1: First to load 100: Last to load) + Returns: - Int: Order priority + int: Order priority """ - return self.data.get('order') + + return self.order @property def loader_name(self): From 7e8e61c0e4d51334d6de0d1f9cd672fa0dae5313 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:14 +0200 Subject: [PATCH 068/122] changed 'get_data' docstring --- openpype/pipeline/workfile/abstract_template_loader.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index fe1f15c140..66943eafe7 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -537,10 +537,12 @@ class AbstractPlaceholder: pass @abstractmethod - def get_data(self, node): - """ - Collect placeholders information. + def get_data(self, identifier): + """Collect information about placeholder by identifier. + Args: - node (AnyNode): A unique node decided by Placeholder implementation + identifier (str): A unique placeholder identifier defined by + implementation. """ + pass From a1cd1890d6db952e4feee357e204444aed0015ed Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:28 +0200 Subject: [PATCH 069/122] modified 'parent_in_hierarchy' docstring --- openpype/pipeline/workfile/abstract_template_loader.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 66943eafe7..a1629d9b79 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -491,13 +491,13 @@ class AbstractPlaceholder: return False @abstractmethod - def parent_in_hierarchy(self, containers): - """Place container in correct hierarchy - given by placeholder + def parent_in_hierarchy(self, container): + """Place loaded container in correct hierarchy given by placeholder + Args: - containers (String): Container name returned back by - placeholder's loader. + container (Dict[str, Any]): Loaded container created by loader. """ + pass @abstractmethod From 56150d4abb72d8b0025a7724e002eab792aa34a0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:48:48 +0200 Subject: [PATCH 070/122] removed unused method 'convert_to_db_filters' --- .../pipeline/workfile/abstract_template_loader.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index a1629d9b79..c36e489017 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -523,19 +523,6 @@ class AbstractPlaceholder: pass - @abstractmethod - def convert_to_db_filters(self, current_asset, linked_asset): - """map current placeholder data as a db filter - args: - current_asset (String): Name of current asset in context - linked asset (list[String]) : Names of assets linked to - current asset in context - Returns: - dict: a dictionnary describing a filter to look for asset in - a database - """ - pass - @abstractmethod def get_data(self, identifier): """Collect information about placeholder by identifier. From 56bbbdbd583b51ba07bac08e753fb5a2050a768f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 8 Aug 2022 17:49:20 +0200 Subject: [PATCH 071/122] removed unused import --- openpype/pipeline/workfile/abstract_template_loader.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index c36e489017..725ab1dab3 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -1,8 +1,6 @@ import os from abc import ABCMeta, abstractmethod -import traceback - import six import logging from functools import reduce From f74101be342ced01df8057f353123692fb559ff3 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Wed, 10 Aug 2022 16:45:19 +0200 Subject: [PATCH 072/122] Remove unused get current renderer logic The `renderer` variable wasn't used --- openpype/hosts/maya/plugins/publish/collect_render.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e6fc8a01e5..e1f4efcc07 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -154,12 +154,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): layer_name = "rs_{}".format(expected_layer_name) # collect all frames we are expecting to be rendered - renderer = self.get_render_attribute("currentRenderer", - layer=layer_name) - # handle various renderman names - if renderer.startswith("renderman"): - renderer = "renderman" - # return all expected files for all cameras and aovs in given # frame range layer_render_products = get_layer_render_products(layer_name) From 02edebad41f26680f0f7ceb3b2b21fe6cfebebab Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 10 Aug 2022 18:33:03 +0200 Subject: [PATCH 073/122] fix import string --- openpype/pipeline/workfile/build_template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/build_template.py b/openpype/pipeline/workfile/build_template.py index df6fe3514a..e6396578c5 100644 --- a/openpype/pipeline/workfile/build_template.py +++ b/openpype/pipeline/workfile/build_template.py @@ -15,7 +15,7 @@ from .build_template_exceptions import ( MissingTemplateLoaderClass ) -_module_path_format = 'openpype.{host}.template_loader' +_module_path_format = 'openpype.hosts.{host}.api.template_loader' def build_workfile_template(*args): From 4f9d1c34e22d22729fc99fc92abcfaeb16ca253b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:39:27 +0200 Subject: [PATCH 074/122] added IHostModule to be able identify module representing a host --- openpype/modules/interfaces.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 334485cab2..424dd158fd 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -1,4 +1,4 @@ -from abc import abstractmethod +from abc import abstractmethod, abstractproperty from openpype import resources @@ -320,3 +320,13 @@ class ISettingsChangeListener(OpenPypeInterface): self, old_value, new_value, changes, project_name, new_value_metadata ): pass + + +class IHostModule(OpenPypeInterface): + """Module which also contain a host implementation.""" + + @abstractproperty + def host_name(self): + """Name of host which module represents.""" + + pass From c86ab4fecfbb6e502723bb86dbbf0748a8135753 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:40:08 +0200 Subject: [PATCH 075/122] added ability to inmport host modules on load modules --- openpype/modules/base.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 1bd343fd07..32fa4d2f31 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -140,7 +140,7 @@ class _LoadCache: def get_default_modules_dir(): """Path to default OpenPype modules.""" - current_dir = os.path.abspath(os.path.dirname(__file__)) + current_dir = os.path.dirname(os.path.abspath(__file__)) output = [] for folder_name in ("default_modules", ): @@ -298,6 +298,8 @@ def _load_modules(): # Add current directory at first place # - has small differences in import logic current_dir = os.path.abspath(os.path.dirname(__file__)) + hosts_dir = os.path.join(os.path.dirname(current_dir), "hosts") + module_dirs.insert(0, hosts_dir) module_dirs.insert(0, current_dir) processed_paths = set() @@ -314,6 +316,7 @@ def _load_modules(): continue is_in_current_dir = dirpath == current_dir + is_in_host_dir = dirpath == hosts_dir for filename in os.listdir(dirpath): # Ignore filenames if filename in IGNORED_FILENAMES: @@ -353,6 +356,24 @@ def _load_modules(): sys.modules[new_import_str] = default_module setattr(openpype_modules, basename, default_module) + elif is_in_host_dir: + import_str = "openpype.hosts.{}".format(basename) + new_import_str = "{}.{}".format(modules_key, basename) + # Until all hosts are converted to be able use them as + # modules is this error check needed + try: + default_module = __import__( + import_str, fromlist=("", ) + ) + sys.modules[new_import_str] = default_module + setattr(openpype_modules, basename, default_module) + + except Exception: + log.warning( + "Failed to import host folder {}".format(basename), + exc_info=True + ) + elif os.path.isdir(fullpath): import_module_from_dirpath(dirpath, filename, modules_key) From 5736b9133cd8f2b2a62146cf6c9fb8310a74f4b5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:40:32 +0200 Subject: [PATCH 076/122] added helper methods to be able get host module by host name --- openpype/modules/base.py | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/openpype/modules/base.py b/openpype/modules/base.py index 32fa4d2f31..ef577e5aa2 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -825,6 +825,45 @@ class ModulesManager: output.extend(hook_paths) return output + def get_host_module(self, host_name): + """Find host module by host name. + + Args: + host_name (str): Host name for which is found host module. + + Returns: + OpenPypeModule: Found host module by name. + None: There was not found module inheriting IHostModule which has + host name set to passed 'host_name'. + """ + + from openpype_interfaces import IHostModule + + for module in self.get_enabled_modules(): + if ( + isinstance(module, IHostModule) + and module.host_name == host_name + ): + return module + return None + + def get_host_names(self): + """List of available host names based on host modules. + + Returns: + Iterable[str]: All available host names based on enabled modules + inheriting 'IHostModule'. + """ + + from openpype_interfaces import IHostModule + + host_names = { + module.host_name + for module in self.get_enabled_modules() + if isinstance(module, IHostModule) + } + return host_names + def print_report(self): """Print out report of time spent on modules initialization parts. From a2dadc85bd51c2fc25baf5098ec5fdcf08e00269 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:42:54 +0200 Subject: [PATCH 077/122] added 'OpenPypeMaya' module --- openpype/hosts/maya/__init__.py | 7 +++++++ openpype/hosts/maya/module.py | 10 ++++++++++ 2 files changed, 17 insertions(+) create mode 100644 openpype/hosts/maya/module.py diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index c1c82c62e5..2178534b89 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,4 +1,5 @@ import os +from .module import OpenPypeMaya def add_implementation_envs(env, _app): @@ -25,3 +26,9 @@ def add_implementation_envs(env, _app): for key, value in defaults.items(): if not env.get(key): env[key] = value + + +__all__ = ( + "OpenPypeMaya", + "add_implementation_envs", +) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py new file mode 100644 index 0000000000..8dfd96d4ab --- /dev/null +++ b/openpype/hosts/maya/module.py @@ -0,0 +1,10 @@ +from openpype.modules import OpenPypeModule +from openpype.modules.interfaces import IHostModule + + +class OpenPypeMaya(OpenPypeModule, IHostModule): + name = "openpype_maya" + host_name = "maya" + + def initialize(self, module_settings): + self.enabled = True From 88be0405986196894b16ae5cb98d303d3d0e9598 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 12:43:50 +0200 Subject: [PATCH 078/122] modev 'add_implementation_envs' to maya module and application knows that it should look there --- openpype/hosts/maya/__init__.py | 28 ---------------------------- openpype/hosts/maya/module.py | 27 +++++++++++++++++++++++++++ openpype/lib/applications.py | 6 ++++-- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/openpype/hosts/maya/__init__.py b/openpype/hosts/maya/__init__.py index 2178534b89..72b4d5853c 100644 --- a/openpype/hosts/maya/__init__.py +++ b/openpype/hosts/maya/__init__.py @@ -1,34 +1,6 @@ -import os from .module import OpenPypeMaya -def add_implementation_envs(env, _app): - # Add requirements to PYTHONPATH - pype_root = os.environ["OPENPYPE_REPOS_ROOT"] - new_python_paths = [ - os.path.join(pype_root, "openpype", "hosts", "maya", "startup") - ] - old_python_path = env.get("PYTHONPATH") or "" - for path in old_python_path.split(os.pathsep): - if not path: - continue - - norm_path = os.path.normpath(path) - if norm_path not in new_python_paths: - new_python_paths.append(norm_path) - - env["PYTHONPATH"] = os.pathsep.join(new_python_paths) - - # Set default values if are not already set via settings - defaults = { - "OPENPYPE_LOG_NO_COLORS": "Yes" - } - for key, value in defaults.items(): - if not env.get(key): - env[key] = value - - __all__ = ( "OpenPypeMaya", - "add_implementation_envs", ) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 8dfd96d4ab..0af68788bc 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -1,6 +1,9 @@ +import os from openpype.modules import OpenPypeModule from openpype.modules.interfaces import IHostModule +MAYA_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + class OpenPypeMaya(OpenPypeModule, IHostModule): name = "openpype_maya" @@ -8,3 +11,27 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): def initialize(self, module_settings): self.enabled = True + + def add_implementation_envs(self, env, _app): + # Add requirements to PYTHONPATH + new_python_paths = [ + os.path.join(MAYA_ROOT_DIR, "startup") + ] + old_python_path = env.get("PYTHONPATH") or "" + for path in old_python_path.split(os.pathsep): + if not path: + continue + + norm_path = os.path.normpath(path) + if norm_path not in new_python_paths: + new_python_paths.append(norm_path) + + env["PYTHONPATH"] = os.pathsep.join(new_python_paths) + + # Set default values if are not already set via settings + defaults = { + "OPENPYPE_LOG_NO_COLORS": "Yes" + } + for key, value in defaults.items(): + if not env.get(key): + env[key] = value diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index da8623ea13..e47ec8cd11 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1508,8 +1508,10 @@ def prepare_app_environments( final_env = None # Add host specific environments if app.host_name and implementation_envs: - module = __import__("openpype.hosts", fromlist=[app.host_name]) - host_module = getattr(module, app.host_name, None) + host_module = modules_manager.get_host_module(app.host_name) + if not host_module: + module = __import__("openpype.hosts", fromlist=[app.host_name]) + host_module = getattr(module, app.host_name, None) add_implementation_envs = None if host_module: add_implementation_envs = getattr( From 32176ba234cf1bff28e15c4efce51cc00d641037 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:29:06 +0200 Subject: [PATCH 079/122] modules does not have to inherit from ILaunchHookPaths and application is passed to 'collect_launch_hook_paths --- openpype/lib/applications.py | 4 +++- openpype/modules/base.py | 38 ++++++++++++++++++++++++++++------ openpype/modules/interfaces.py | 33 ++++++++++++++++++++++++++++- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e47ec8cd11..5443320960 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -996,7 +996,9 @@ class ApplicationLaunchContext: paths.append(path) # Load modules paths - paths.extend(self.modules_manager.collect_launch_hook_paths()) + paths.extend( + self.modules_manager.collect_launch_hook_paths(self.application) + ) return paths diff --git a/openpype/modules/base.py b/openpype/modules/base.py index ef577e5aa2..e26075283d 100644 --- a/openpype/modules/base.py +++ b/openpype/modules/base.py @@ -789,24 +789,50 @@ class ModulesManager: output.extend(paths) return output - def collect_launch_hook_paths(self): - """Helper to collect hooks from modules inherited ILaunchHookPaths. + def collect_launch_hook_paths(self, app): + """Helper to collect application launch hooks. + + It used to be based on 'ILaunchHookPaths' which is not true anymore. + Module just have to have implemented 'get_launch_hook_paths' method. + + Args: + app (Application): Application object which can be used for + filtering of which launch hook paths are returned. Returns: list: Paths to launch hook directories. """ - from openpype_interfaces import ILaunchHookPaths str_type = type("") expected_types = (list, tuple, set) output = [] for module in self.get_enabled_modules(): - # Skip module that do not inherit from `ILaunchHookPaths` - if not isinstance(module, ILaunchHookPaths): + # Skip module if does not have implemented 'get_launch_hook_paths' + func = getattr(module, "get_launch_hook_paths", None) + if func is None: + continue + + func = module.get_launch_hook_paths + if hasattr(inspect, "signature"): + sig = inspect.signature(func) + expect_args = len(sig.parameters) > 0 + else: + expect_args = len(inspect.getargspec(func)[0]) > 0 + + # Pass application argument if method expect it. + try: + if expect_args: + hook_paths = func(app) + else: + hook_paths = func() + except Exception: + self.log.warning( + "Failed to call 'get_launch_hook_paths'", + exc_info=True + ) continue - hook_paths = module.get_launch_hook_paths() if not hook_paths: continue diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index 424dd158fd..de9ba13800 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -50,12 +50,32 @@ class IPluginPaths(OpenPypeInterface): class ILaunchHookPaths(OpenPypeInterface): """Module has launch hook paths to return. + Modules does not have to inherit from this interface (changed 8.11.2022). + Module just have to have implemented 'get_launch_hook_paths' to be able use + the advantage. + Expected result is list of paths. ["path/to/launch_hooks_dir"] """ @abstractmethod - def get_launch_hook_paths(self): + def get_launch_hook_paths(self, app): + """Paths to directory with application launch hooks. + + Method can be also defined without arguments. + ```python + def get_launch_hook_paths(self): + return [] + ``` + + Args: + app (Application): Application object which can be used for + filtering of which launch hook paths are returned. + + Returns: + Iterable[str]: Path to directories where launch hooks can be found. + """ + pass @@ -66,6 +86,7 @@ class ITrayModule(OpenPypeInterface): The module still must be usable if is not used in tray even if would do nothing. """ + tray_initialized = False _tray_manager = None @@ -78,16 +99,19 @@ class ITrayModule(OpenPypeInterface): This is where GUIs should be loaded or tray specific parts should be prepared. """ + pass @abstractmethod def tray_menu(self, tray_menu): """Add module's action to tray menu.""" + pass @abstractmethod def tray_start(self): """Start procedure in Pype tray.""" + pass @abstractmethod @@ -96,6 +120,7 @@ class ITrayModule(OpenPypeInterface): This is place where all threads should be shut. """ + pass def execute_in_main_thread(self, callback): @@ -104,6 +129,7 @@ class ITrayModule(OpenPypeInterface): Some callbacks need to be processed on main thread (menu actions must be added on main thread or they won't get triggered etc.) """ + if not self.tray_initialized: # TODO Called without initialized tray, still main thread needed try: @@ -128,6 +154,7 @@ class ITrayModule(OpenPypeInterface): msecs (int): Duration of message visibility in miliseconds. Default is 10000 msecs, may differ by Qt version. """ + if self._tray_manager: self._tray_manager.show_tray_message(title, message, icon, msecs) @@ -280,16 +307,19 @@ class ITrayService(ITrayModule): def set_service_running_icon(self): """Change icon of an QAction to green circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_running()) def set_service_failed_icon(self): """Change icon of an QAction to red circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_failed()) def set_service_idle_icon(self): """Change icon of an QAction to orange circle.""" + if self.menu_action: self.menu_action.setIcon(self.get_icon_idle()) @@ -303,6 +333,7 @@ class ISettingsChangeListener(OpenPypeInterface): "publish": ["path/to/publish_plugins"] } """ + @abstractmethod def on_system_settings_save( self, old_value, new_value, changes, new_value_metadata From 0ae844401cc271ef0edf9b16a5dda4893dd7bcfd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:29:35 +0200 Subject: [PATCH 080/122] maya is registering it's launch hooks --- openpype/hosts/maya/module.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index 0af68788bc..e058f1cef5 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -35,3 +35,10 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): for key, value in defaults.items(): if not env.get(key): env[key] = value + + def get_launch_hook_paths(self, app): + if app.host_name != self.host_name: + return [] + return [ + os.path.join(MAYA_ROOT_DIR, "hooks") + ] From 58af54c4437d0495f2f00c7962455bd8cdbf1a1a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:33:35 +0200 Subject: [PATCH 081/122] let host module add it's prelaunch hooks and don't guess it --- openpype/lib/applications.py | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index 5443320960..e23cc6215f 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -962,32 +962,24 @@ class ApplicationLaunchContext: # TODO load additional studio paths from settings import openpype - pype_dir = os.path.dirname(os.path.abspath(openpype.__file__)) + openpype_dir = os.path.dirname(os.path.abspath(openpype.__file__)) - # --- START: Backwards compatibility --- - hooks_dir = os.path.join(pype_dir, "hooks") + global_hooks_dir = os.path.join(openpype_dir, "hooks") - subfolder_names = ["global"] - if self.host_name: - subfolder_names.append(self.host_name) - for subfolder_name in subfolder_names: - path = os.path.join(hooks_dir, subfolder_name) - if ( - os.path.exists(path) - and os.path.isdir(path) - and path not in paths - ): - paths.append(path) - # --- END: Backwards compatibility --- - - subfolders_list = [ - ["hooks"] + hooks_dirs = [ + global_hooks_dir ] if self.host_name: - subfolders_list.append(["hosts", self.host_name, "hooks"]) + # If host requires launch hooks and is module then launch hooks + # should be collected using 'collect_launch_hook_paths' + # - module have to implement 'get_launch_hook_paths' + host_module = self.modules_manager.get_host_module(self.host_name) + if not host_module: + hooks_dirs.append(os.path.join( + openpype_dir, "hosts", self.host_name, "hooks" + )) - for subfolders in subfolders_list: - path = os.path.join(pype_dir, *subfolders) + for path in hooks_dirs: if ( os.path.exists(path) and os.path.isdir(path) From 7d304d0f8695775e6f3e49d2b0271ac2b8564883 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 13:39:32 +0200 Subject: [PATCH 082/122] host module can define workfile extensions --- openpype/lib/applications.py | 23 ++++++++++++++++------- openpype/modules/interfaces.py | 11 +++++++++++ 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/openpype/lib/applications.py b/openpype/lib/applications.py index e23cc6215f..0f380d0f4b 100644 --- a/openpype/lib/applications.py +++ b/openpype/lib/applications.py @@ -1303,6 +1303,7 @@ def get_app_environments_for_context( dict: Environments for passed context and application. """ + from openpype.modules import ModulesManager from openpype.pipeline import AvalonMongoDB, Anatomy # Avalon database connection @@ -1315,8 +1316,6 @@ def get_app_environments_for_context( asset_doc = get_asset_by_name(project_name, asset_name) if modules_manager is None: - from openpype.modules import ModulesManager - modules_manager = ModulesManager() # Prepare app object which can be obtained only from ApplciationManager @@ -1343,7 +1342,7 @@ def get_app_environments_for_context( }) prepare_app_environments(data, env_group, modules_manager) - prepare_context_environments(data, env_group) + prepare_context_environments(data, env_group, modules_manager) # Discard avalon connection dbcon.uninstall() @@ -1564,7 +1563,7 @@ def apply_project_environments_value( return env -def prepare_context_environments(data, env_group=None): +def prepare_context_environments(data, env_group=None, modules_manager=None): """Modify launch environments with context data for launched host. Args: @@ -1652,10 +1651,10 @@ def prepare_context_environments(data, env_group=None): data["env"]["AVALON_APP"] = app.host_name data["env"]["AVALON_WORKDIR"] = workdir - _prepare_last_workfile(data, workdir) + _prepare_last_workfile(data, workdir, modules_manager) -def _prepare_last_workfile(data, workdir): +def _prepare_last_workfile(data, workdir, modules_manager): """last workfile workflow preparation. Function check if should care about last workfile workflow and tries @@ -1670,8 +1669,13 @@ def _prepare_last_workfile(data, workdir): result will be stored. workdir (str): Path to folder where workfiles should be stored. """ + + from openpype.modules import ModulesManager from openpype.pipeline import HOST_WORKFILE_EXTENSIONS + if not modules_manager: + modules_manager = ModulesManager() + log = data["log"] _workdir_data = data.get("workdir_data") @@ -1719,7 +1723,12 @@ def _prepare_last_workfile(data, workdir): # Last workfile path last_workfile_path = data.get("last_workfile_path") or "" if not last_workfile_path: - extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) + host_module = modules_manager.get_host_module(app.host_name) + if host_module: + extensions = host_module.get_workfile_extensions() + else: + extensions = HOST_WORKFILE_EXTENSIONS.get(app.host_name) + if extensions: anatomy = data["anatomy"] project_settings = data["project_settings"] diff --git a/openpype/modules/interfaces.py b/openpype/modules/interfaces.py index de9ba13800..14f49204ee 100644 --- a/openpype/modules/interfaces.py +++ b/openpype/modules/interfaces.py @@ -361,3 +361,14 @@ class IHostModule(OpenPypeInterface): """Name of host which module represents.""" pass + + def get_workfile_extensions(self): + """Define workfile extensions for host. + + Not all hosts support workfiles thus this is optional implementation. + + Returns: + List[str]: Extensions used for workfiles with dot. + """ + + return [] From 9b623c1dd3e335aeb48d3428f6a0cba5e5793e51 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 14:14:21 +0200 Subject: [PATCH 083/122] maya define it's workfile extensions only in module itself --- openpype/hosts/maya/api/workio.py | 4 +--- openpype/hosts/maya/module.py | 3 +++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/maya/api/workio.py b/openpype/hosts/maya/api/workio.py index fd4961c4bf..8c31974c73 100644 --- a/openpype/hosts/maya/api/workio.py +++ b/openpype/hosts/maya/api/workio.py @@ -2,11 +2,9 @@ import os from maya import cmds -from openpype.pipeline import HOST_WORKFILE_EXTENSIONS - def file_extensions(): - return HOST_WORKFILE_EXTENSIONS["maya"] + return [".ma", ".mb"] def has_unsaved_changes(): diff --git a/openpype/hosts/maya/module.py b/openpype/hosts/maya/module.py index e058f1cef5..5a215be8d2 100644 --- a/openpype/hosts/maya/module.py +++ b/openpype/hosts/maya/module.py @@ -42,3 +42,6 @@ class OpenPypeMaya(OpenPypeModule, IHostModule): return [ os.path.join(MAYA_ROOT_DIR, "hooks") ] + + def get_workfile_extensions(self): + return [".ma", ".mb"] From 25616886bff2b6fda0b4c9646ea9256389ba248f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:08:14 +0200 Subject: [PATCH 084/122] raise and error when nothing is selected --- openpype/hosts/maya/api/lib_template_builder.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/hosts/maya/api/lib_template_builder.py b/openpype/hosts/maya/api/lib_template_builder.py index 855c72e361..34a8450a26 100644 --- a/openpype/hosts/maya/api/lib_template_builder.py +++ b/openpype/hosts/maya/api/lib_template_builder.py @@ -40,6 +40,9 @@ def create_placeholder(): placeholder_name = create_placeholder_name(args, options) selection = cmds.ls(selection=True) + if not selection: + raise ValueError("Nothing is selected") + placeholder = cmds.spaceLocator(name=placeholder_name)[0] # get the long name of the placeholder (with the groups) From 683468c5633a42b8c5e80510ab060f981452d02c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:22:08 +0200 Subject: [PATCH 085/122] use 'filter_profiles' function for profiles filtering --- .../workfile/abstract_template_loader.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 725ab1dab3..51d06cdb3f 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -7,7 +7,11 @@ from functools import reduce from openpype.client import get_asset_by_name from openpype.settings import get_project_settings -from openpype.lib import get_linked_assets, PypeLogger as Logger +from openpype.lib import ( + Logger, + filter_profiles, + get_linked_assets, +) from openpype.pipeline import legacy_io, Anatomy from openpype.pipeline.load import ( get_loaders_by_name, @@ -167,22 +171,23 @@ class AbstractTemplateLoader: anatomy = Anatomy(project_name) project_settings = get_project_settings(project_name) - build_info = project_settings[host_name]['templated_workfile_build'] - profiles = build_info['profiles'] + build_info = project_settings[host_name]["templated_workfile_build"] + profile = filter_profiles( + build_info["profiles"], + { + "task_types": task_type, + "tasks": task_name + } + ) - for prf in profiles: - if prf['task_types'] and task_type not in prf['task_types']: - continue - if prf['tasks'] and task_name not in prf['tasks']: - continue - path = prf['path'] - break - else: # IF no template were found (no break happened) + if not profile: raise TemplateProfileNotFound( "No matching profile found for task '{}' of type '{}' " "with host '{}'".format(task_name, task_type, host_name) ) - if path is None: + + path = profile["path"] + if not path: raise TemplateLoadingFailed( "Template path is not set.\n" "Path need to be set in {}\\Template Workfile Build " From bb9a16100acd9a7d94ec6ff6ea15891916eea580 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:22:23 +0200 Subject: [PATCH 086/122] removed unnecessary finally statement --- openpype/pipeline/workfile/abstract_template_loader.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 51d06cdb3f..0ed32033af 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -205,9 +205,8 @@ class AbstractTemplateLoader: raise KeyError( "Could not solve key '{}' in template path '{}'".format( missing_key, path)) - finally: - solved_path = os.path.normpath(solved_path) + solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): raise TemplateNotFound( "Template found in openPype settings for task '{}' with host " From 12a8307a8331334ee9700efba2127211ea332ff0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:40:02 +0200 Subject: [PATCH 087/122] simplified path formatting --- .../workfile/abstract_template_loader.py | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 0ed32033af..5afec56d71 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -8,6 +8,7 @@ from functools import reduce from openpype.client import get_asset_by_name from openpype.settings import get_project_settings from openpype.lib import ( + StringTemplate, Logger, filter_profiles, get_linked_assets, @@ -192,19 +193,35 @@ class AbstractTemplateLoader: "Template path is not set.\n" "Path need to be set in {}\\Template Workfile Build " "Settings\\Profiles".format(host_name.title())) - try: - solved_path = None - while True: + + # Try fill path with environments and anatomy roots + fill_data = { + key: value + for key, value in os.environ.items() + } + fill_data["root"] = anatomy.roots + result = StringTemplate.format_template(path, fill_data) + if result.solved: + path = result.normalized() + + if path and os.path.exists(path): + self.log.info("Found template at: '{}'".format(path)) + return path + + solved_path = None + while True: + try: solved_path = anatomy.path_remapper(path) - if solved_path is None: - solved_path = path - if solved_path == path: - break - path = solved_path - except KeyError as missing_key: - raise KeyError( - "Could not solve key '{}' in template path '{}'".format( - missing_key, path)) + except KeyError as missing_key: + raise KeyError( + "Could not solve key '{}' in template path '{}'".format( + missing_key, path)) + + if solved_path is None: + solved_path = path + if solved_path == path: + break + path = solved_path solved_path = os.path.normpath(solved_path) if not os.path.exists(solved_path): @@ -213,7 +230,7 @@ class AbstractTemplateLoader: "'{}' does not exists. (Not found : {})".format( task_name, host_name, solved_path)) - self.log.info("Found template at : '{}'".format(solved_path)) + self.log.info("Found template at: '{}'".format(solved_path)) return solved_path From 748dcf1ad207edd3dbf3bc98120d9e46bf9b39e0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:51:27 +0200 Subject: [PATCH 088/122] fix filter and sort --- .../workfile/abstract_template_loader.py | 29 ++++++------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 5afec56d71..1c8ede25e6 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -351,11 +351,15 @@ class AbstractTemplateLoader: self.populate_template(ignored_ids=loaded_containers_ids) def get_placeholders(self): - placeholder_class = self.placeholder_class - placeholders = map(placeholder_class, self.get_template_nodes()) - valid_placeholders = filter(placeholder_class.is_valid, placeholders) - sorted_placeholders = sorted(valid_placeholders, - key=placeholder_class.get_order) + placeholders = map(self.placeholder_class, self.get_template_nodes()) + valid_placeholders = filter( + lambda i: i.is_valid, + placeholders + ) + sorted_placeholders = list(sorted( + valid_placeholders, + key=lambda i: i.order + )) return sorted_placeholders @abstractmethod @@ -450,21 +454,6 @@ class AbstractPlaceholder: def order(self): return self.data["order"] - def get_order(self): - """Placeholder order. - - Order is used to sort them by priority - Priority is lowset first, highest last - (ex: - 1: First to load - 100: Last to load) - - Returns: - int: Order priority - """ - - return self.order - @property def loader_name(self): """Return placeholder loader type. From 7eaa278c741ceaa30daf056dd17ec9e4b4ceed10 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 15:54:19 +0200 Subject: [PATCH 089/122] removed invalid default setting for templates --- openpype/settings/defaults/project_settings/maya.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/openpype/settings/defaults/project_settings/maya.json b/openpype/settings/defaults/project_settings/maya.json index 9c2c737ece..e9109abd22 100644 --- a/openpype/settings/defaults/project_settings/maya.json +++ b/openpype/settings/defaults/project_settings/maya.json @@ -123,7 +123,6 @@ "defaults": [ "Main" ] - }, "CreateAss": { "enabled": true, @@ -969,13 +968,7 @@ ] }, "templated_workfile_build": { - "profiles": [ - { - "task_types": [], - "tasks": [], - "path": "/path/to/your/template" - } - ] + "profiles": [] }, "filters": { "preset 1": { From 66ee0beaf6d0e09eae6a8a9887a90651618a73f2 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 17:43:47 +0200 Subject: [PATCH 090/122] fix empty or query --- openpype/client/entities.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index f9d3badb1a..c798c0ad6d 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1130,11 +1130,12 @@ def _get_representations( for item in _regex_filters(flatten_filters): for key, value in item.items(): - if key == "$or": - or_queries.append(value) - else: + if key != "$or": query_filter[key] = value + elif value: + or_queries.append(value) + if len(or_queries) == 1: query_filter["$or"] = or_queries[0] elif or_queries: From 8ba5d0079952731a2e7a98490701750bffa28a9e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 18:59:43 +0200 Subject: [PATCH 091/122] move env setup function used in prelaunch hook from api higher --- openpype/hosts/resolve/utils.py | 54 +++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 openpype/hosts/resolve/utils.py diff --git a/openpype/hosts/resolve/utils.py b/openpype/hosts/resolve/utils.py new file mode 100644 index 0000000000..382a7cf344 --- /dev/null +++ b/openpype/hosts/resolve/utils.py @@ -0,0 +1,54 @@ +import os +import shutil +from openpype.lib import Logger + +RESOLVE_ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + + +def setup(env): + log = Logger.get_logger("ResolveSetup") + scripts = {} + us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") + us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") + us_paths = [os.path.join( + RESOLVE_ROOT_DIR, + "utility_scripts" + )] + + # collect script dirs + if us_env: + log.info(f"Utility Scripts Env: `{us_env}`") + us_paths = us_env.split( + os.pathsep) + us_paths + + # collect scripts from dirs + for path in us_paths: + scripts.update({path: os.listdir(path)}) + + log.info(f"Utility Scripts Dir: `{us_paths}`") + log.info(f"Utility Scripts: `{scripts}`") + + # make sure no script file is in folder + for s in os.listdir(us_dir): + path = os.path.join(us_dir, s) + log.info(f"Removing `{path}`...") + if os.path.isdir(path): + shutil.rmtree(path, onerror=None) + else: + os.remove(path) + + # copy scripts into Resolve's utility scripts dir + for d, sl in scripts.items(): + # directory and scripts list + for s in sl: + # script in script list + src = os.path.join(d, s) + dst = os.path.join(us_dir, s) + log.info(f"Copying `{src}` to `{dst}`...") + if os.path.isdir(src): + shutil.copytree( + src, dst, symlinks=False, + ignore=None, ignore_dangling_symlinks=False + ) + else: + shutil.copy2(src, dst) From 07d89fc23b593890d10f7094af2be04399f8dedd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 19:01:24 +0200 Subject: [PATCH 092/122] fixed imports to use in-DCC imports from resolve.api --- openpype/hosts/resolve/__init__.py | 129 ----------------- openpype/hosts/resolve/api/__init__.py | 134 +++++++++++++++++- openpype/hosts/resolve/api/action.py | 2 +- openpype/hosts/resolve/api/lib.py | 6 +- openpype/hosts/resolve/api/menu.py | 4 +- openpype/hosts/resolve/api/pipeline.py | 13 +- openpype/hosts/resolve/api/preload_console.py | 4 +- openpype/hosts/resolve/api/utils.py | 96 ++----------- openpype/hosts/resolve/api/workio.py | 19 +-- .../hosts/resolve/hooks/pre_resolve_setup.py | 20 +-- .../plugins/create/create_shot_clip.py | 15 +- .../hosts/resolve/plugins/load/load_clip.py | 21 +-- .../plugins/publish/extract_workfile.py | 4 +- .../plugins/publish/precollect_instances.py | 26 ++-- .../OpenPype_sync_util_scripts.py | 5 +- .../utility_scripts/__OpenPype__Menu__.py | 6 +- .../utility_scripts/tests/test_otio_as_edl.py | 4 +- .../testing_create_timeline_item_from_path.py | 15 +- .../tests/testing_load_media_pool_item.py | 10 +- 19 files changed, 233 insertions(+), 300 deletions(-) diff --git a/openpype/hosts/resolve/__init__.py b/openpype/hosts/resolve/__init__.py index 3e49ce3b9b..e69de29bb2 100644 --- a/openpype/hosts/resolve/__init__.py +++ b/openpype/hosts/resolve/__init__.py @@ -1,129 +0,0 @@ -from .api.utils import ( - setup, - get_resolve_module -) - -from .api.pipeline import ( - install, - uninstall, - ls, - containerise, - update_container, - publish, - launch_workfiles_app, - maintained_selection, - remove_instance, - list_instances -) - -from .api.lib import ( - maintain_current_timeline, - publish_clip_color, - get_project_manager, - get_current_project, - get_current_timeline, - create_bin, - get_media_pool_item, - create_media_pool_item, - create_timeline_item, - get_timeline_item, - get_video_track_names, - get_current_timeline_items, - get_pype_timeline_item_by_name, - get_timeline_item_pype_tag, - set_timeline_item_pype_tag, - imprint, - set_publish_attribute, - get_publish_attribute, - create_compound_clip, - swap_clips, - get_pype_clip_metadata, - set_project_manager_to_folder_name, - get_otio_clip_instance_data, - get_reformated_path -) - -from .api.menu import launch_pype_menu - -from .api.plugin import ( - ClipLoader, - TimelineItemLoader, - Creator, - PublishClip -) - -from .api.workio import ( - open_file, - save_file, - current_file, - has_unsaved_changes, - file_extensions, - work_root -) - -from .api.testing_utils import TestGUI - - -__all__ = [ - # pipeline - "install", - "uninstall", - "ls", - "containerise", - "update_container", - "reload_pipeline", - "publish", - "launch_workfiles_app", - "maintained_selection", - "remove_instance", - "list_instances", - - # utils - "setup", - "get_resolve_module", - - # lib - "maintain_current_timeline", - "publish_clip_color", - "get_project_manager", - "get_current_project", - "get_current_timeline", - "create_bin", - "get_media_pool_item", - "create_media_pool_item", - "create_timeline_item", - "get_timeline_item", - "get_video_track_names", - "get_current_timeline_items", - "get_pype_timeline_item_by_name", - "get_timeline_item_pype_tag", - "set_timeline_item_pype_tag", - "imprint", - "set_publish_attribute", - "get_publish_attribute", - "create_compound_clip", - "swap_clips", - "get_pype_clip_metadata", - "set_project_manager_to_folder_name", - "get_otio_clip_instance_data", - "get_reformated_path", - - # menu - "launch_pype_menu", - - # plugin - "ClipLoader", - "TimelineItemLoader", - "Creator", - "PublishClip", - - # workio - "open_file", - "save_file", - "current_file", - "has_unsaved_changes", - "file_extensions", - "work_root", - - "TestGUI" -] diff --git a/openpype/hosts/resolve/api/__init__.py b/openpype/hosts/resolve/api/__init__.py index 48bd938e57..cf1edb4c35 100644 --- a/openpype/hosts/resolve/api/__init__.py +++ b/openpype/hosts/resolve/api/__init__.py @@ -1,11 +1,137 @@ """ resolve api """ -import os bmdvr = None bmdvf = None -API_DIR = os.path.dirname(os.path.abspath(__file__)) -HOST_DIR = os.path.dirname(API_DIR) -PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") +from .utils import ( + get_resolve_module +) + +from .pipeline import ( + install, + uninstall, + ls, + containerise, + update_container, + publish, + launch_workfiles_app, + maintained_selection, + remove_instance, + list_instances +) + +from .lib import ( + maintain_current_timeline, + publish_clip_color, + get_project_manager, + get_current_project, + get_current_timeline, + create_bin, + get_media_pool_item, + create_media_pool_item, + create_timeline_item, + get_timeline_item, + get_video_track_names, + get_current_timeline_items, + get_pype_timeline_item_by_name, + get_timeline_item_pype_tag, + set_timeline_item_pype_tag, + imprint, + set_publish_attribute, + get_publish_attribute, + create_compound_clip, + swap_clips, + get_pype_clip_metadata, + set_project_manager_to_folder_name, + get_otio_clip_instance_data, + get_reformated_path +) + +from .menu import launch_pype_menu + +from .plugin import ( + ClipLoader, + TimelineItemLoader, + Creator, + PublishClip +) + +from .workio import ( + open_file, + save_file, + current_file, + has_unsaved_changes, + file_extensions, + work_root +) + +from .testing_utils import TestGUI + + +__all__ = [ + "bmdvr", + "bmdvf", + + # pipeline + "install", + "uninstall", + "ls", + "containerise", + "update_container", + "reload_pipeline", + "publish", + "launch_workfiles_app", + "maintained_selection", + "remove_instance", + "list_instances", + + # utils + "get_resolve_module", + + # lib + "maintain_current_timeline", + "publish_clip_color", + "get_project_manager", + "get_current_project", + "get_current_timeline", + "create_bin", + "get_media_pool_item", + "create_media_pool_item", + "create_timeline_item", + "get_timeline_item", + "get_video_track_names", + "get_current_timeline_items", + "get_pype_timeline_item_by_name", + "get_timeline_item_pype_tag", + "set_timeline_item_pype_tag", + "imprint", + "set_publish_attribute", + "get_publish_attribute", + "create_compound_clip", + "swap_clips", + "get_pype_clip_metadata", + "set_project_manager_to_folder_name", + "get_otio_clip_instance_data", + "get_reformated_path", + + # menu + "launch_pype_menu", + + # plugin + "ClipLoader", + "TimelineItemLoader", + "Creator", + "PublishClip", + + # workio + "open_file", + "save_file", + "current_file", + "has_unsaved_changes", + "file_extensions", + "work_root", + + "TestGUI" +] diff --git a/openpype/hosts/resolve/api/action.py b/openpype/hosts/resolve/api/action.py index f8f338a850..d55a24a39a 100644 --- a/openpype/hosts/resolve/api/action.py +++ b/openpype/hosts/resolve/api/action.py @@ -4,7 +4,7 @@ from __future__ import absolute_import import pyblish.api -from ...action import get_errored_instances_from_context +from openpype.action import get_errored_instances_from_context class SelectInvalidAction(pyblish.api.Action): diff --git a/openpype/hosts/resolve/api/lib.py b/openpype/hosts/resolve/api/lib.py index 93ccdaf812..f41eb36caf 100644 --- a/openpype/hosts/resolve/api/lib.py +++ b/openpype/hosts/resolve/api/lib.py @@ -4,13 +4,13 @@ import re import os import contextlib from opentimelineio import opentime + +from openpype.lib import Logger from openpype.pipeline.editorial import is_overlapping_otio_ranges from ..otio import davinci_export as otio_export -from openpype.api import Logger - -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) self = sys.modules[__name__] self.project_manager = None diff --git a/openpype/hosts/resolve/api/menu.py b/openpype/hosts/resolve/api/menu.py index 9e0dd12376..2c7678ee5b 100644 --- a/openpype/hosts/resolve/api/menu.py +++ b/openpype/hosts/resolve/api/menu.py @@ -3,13 +3,13 @@ import sys from Qt import QtWidgets, QtCore +from openpype.tools.utils import host_tools + from .pipeline import ( publish, launch_workfiles_app ) -from openpype.tools.utils import host_tools - def load_stylesheet(): path = os.path.join(os.path.dirname(__file__), "menu_style.qss") diff --git a/openpype/hosts/resolve/api/pipeline.py b/openpype/hosts/resolve/api/pipeline.py index 4a7d1c5bea..1c8d9dc01c 100644 --- a/openpype/hosts/resolve/api/pipeline.py +++ b/openpype/hosts/resolve/api/pipeline.py @@ -7,7 +7,7 @@ from collections import OrderedDict from pyblish import api as pyblish -from openpype.api import Logger +from openpype.lib import Logger from openpype.pipeline import ( schema, register_loader_plugin_path, @@ -16,11 +16,15 @@ from openpype.pipeline import ( deregister_creator_plugin_path, AVALON_CONTAINER_ID, ) -from . import lib -from . import PLUGINS_DIR from openpype.tools.utils import host_tools -log = Logger().get_logger(__name__) +from . import lib +from .utils import get_resolve_module + +log = Logger.get_logger(__name__) + +HOST_DIR = os.path.dirname(os.path.abspath(os.path.dirname(__file__))) +PLUGINS_DIR = os.path.join(HOST_DIR, "plugins") PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") LOAD_PATH = os.path.join(PLUGINS_DIR, "load") CREATE_PATH = os.path.join(PLUGINS_DIR, "create") @@ -39,7 +43,6 @@ def install(): See the Maya equivalent for inspiration on how to implement this. """ - from .. import get_resolve_module log.info("openpype.hosts.resolve installed") diff --git a/openpype/hosts/resolve/api/preload_console.py b/openpype/hosts/resolve/api/preload_console.py index 1e3a56b4dd..a822ea2460 100644 --- a/openpype/hosts/resolve/api/preload_console.py +++ b/openpype/hosts/resolve/api/preload_console.py @@ -1,9 +1,9 @@ #!/usr/bin/env python import time from openpype.hosts.resolve.utils import get_resolve_module -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) wait_delay = 2.5 wait = 0.00 diff --git a/openpype/hosts/resolve/api/utils.py b/openpype/hosts/resolve/api/utils.py index 9b3762f328..871b3af38d 100644 --- a/openpype/hosts/resolve/api/utils.py +++ b/openpype/hosts/resolve/api/utils.py @@ -4,21 +4,21 @@ Resolve's tools for setting environment """ -import sys import os -import shutil -from . import HOST_DIR -from openpype.api import Logger -log = Logger().get_logger(__name__) +import sys + +from openpype.lib import Logger + +log = Logger.get_logger(__name__) def get_resolve_module(): - from openpype.hosts import resolve + from openpype.hosts.resolve import api # dont run if already loaded - if resolve.api.bmdvr: + if api.bmdvr: log.info(("resolve module is assigned to " - f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}")) - return resolve.api.bmdvr + f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) + return api.bmdvr try: """ The PYTHONPATH needs to be set correctly for this import @@ -71,79 +71,9 @@ def get_resolve_module(): # assign global var and return bmdvr = bmd.scriptapp("Resolve") bmdvf = bmd.scriptapp("Fusion") - resolve.api.bmdvr = bmdvr - resolve.api.bmdvf = bmdvf + api.bmdvr = bmdvr + api.bmdvf = bmdvf log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvr`: {resolve.api.bmdvr}")) + f"`pype.hosts.resolve.api.bmdvr`: {api.bmdvr}")) log.info(("Assigning resolve module to " - f"`pype.hosts.resolve.api.bmdvf`: {resolve.api.bmdvf}")) - - -def _sync_utility_scripts(env=None): - """ Synchronizing basic utlility scripts for resolve. - - To be able to run scripts from inside `Resolve/Workspace/Scripts` menu - all scripts has to be accessible from defined folder. - """ - if not env: - env = os.environ - - # initiate inputs - scripts = {} - us_env = env.get("RESOLVE_UTILITY_SCRIPTS_SOURCE_DIR") - us_dir = env.get("RESOLVE_UTILITY_SCRIPTS_DIR", "") - us_paths = [os.path.join( - HOST_DIR, - "utility_scripts" - )] - - # collect script dirs - if us_env: - log.info(f"Utility Scripts Env: `{us_env}`") - us_paths = us_env.split( - os.pathsep) + us_paths - - # collect scripts from dirs - for path in us_paths: - scripts.update({path: os.listdir(path)}) - - log.info(f"Utility Scripts Dir: `{us_paths}`") - log.info(f"Utility Scripts: `{scripts}`") - - # make sure no script file is in folder - if next((s for s in os.listdir(us_dir)), None): - for s in os.listdir(us_dir): - path = os.path.join(us_dir, s) - log.info(f"Removing `{path}`...") - if os.path.isdir(path): - shutil.rmtree(path, onerror=None) - else: - os.remove(path) - - # copy scripts into Resolve's utility scripts dir - for d, sl in scripts.items(): - # directory and scripts list - for s in sl: - # script in script list - src = os.path.join(d, s) - dst = os.path.join(us_dir, s) - log.info(f"Copying `{src}` to `{dst}`...") - if os.path.isdir(src): - shutil.copytree( - src, dst, symlinks=False, - ignore=None, ignore_dangling_symlinks=False - ) - else: - shutil.copy2(src, dst) - - -def setup(env=None): - """ Wrapper installer started from pype.hooks.resolve.ResolvePrelaunch() - """ - if not env: - env = os.environ - - # synchronize resolve utility scripts - _sync_utility_scripts(env) - - log.info("Resolve OpenPype wrapper has been installed") + f"`pype.hosts.resolve.api.bmdvf`: {api.bmdvf}")) diff --git a/openpype/hosts/resolve/api/workio.py b/openpype/hosts/resolve/api/workio.py index f175769387..5a742ecf7e 100644 --- a/openpype/hosts/resolve/api/workio.py +++ b/openpype/hosts/resolve/api/workio.py @@ -2,14 +2,14 @@ import os from openpype.api import Logger -from .. import ( +from .lib import ( get_project_manager, get_current_project, set_project_manager_to_folder_name ) -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) exported_projet_ext = ".drp" @@ -60,7 +60,7 @@ def open_file(filepath): # load project from input path project = pm.LoadProject(fname) log.info(f"Project {project.GetName()} opened...") - return True + except AttributeError: log.warning((f"Project with name `{fname}` does not exist! It will " f"be imported from {filepath} and then loaded...")) @@ -69,9 +69,8 @@ def open_file(filepath): project = pm.LoadProject(fname) log.info(f"Project imported/loaded {project.GetName()}...") return True - else: - return False - + return False + return True def current_file(): pm = get_project_manager() @@ -80,13 +79,9 @@ def current_file(): name = project.GetName() fname = name + exported_projet_ext current_file = os.path.join(current_dir, fname) - normalised = os.path.normpath(current_file) - - # Unsaved current file - if normalised == "": + if not current_file: return None - - return normalised + return os.path.normpath(current_file) def work_root(session): diff --git a/openpype/hosts/resolve/hooks/pre_resolve_setup.py b/openpype/hosts/resolve/hooks/pre_resolve_setup.py index 978e3760fd..1d977e2d8e 100644 --- a/openpype/hosts/resolve/hooks/pre_resolve_setup.py +++ b/openpype/hosts/resolve/hooks/pre_resolve_setup.py @@ -1,7 +1,7 @@ import os -import importlib + from openpype.lib import PreLaunchHook -from openpype.hosts.resolve.api import utils +from openpype.hosts.resolve.utils import setup class ResolvePrelaunch(PreLaunchHook): @@ -43,18 +43,6 @@ class ResolvePrelaunch(PreLaunchHook): self.launch_context.env.get("PRE_PYTHON_SCRIPT", "")) self.launch_context.env["PRE_PYTHON_SCRIPT"] = pre_py_sc self.log.debug(f"-- pre_py_sc: `{pre_py_sc}`...") - try: - __import__("openpype.hosts.resolve") - __import__("pyblish") - except ImportError: - self.log.warning( - "pyblish: Could not load Resolve integration.", - exc_info=True - ) - - else: - # Resolve Setup integration - importlib.reload(utils) - self.log.debug(f"-- utils.__file__: `{utils.__file__}`") - utils.setup(self.launch_context.env) + # Resolve Setup integration + setup(self.launch_context.env) diff --git a/openpype/hosts/resolve/plugins/create/create_shot_clip.py b/openpype/hosts/resolve/plugins/create/create_shot_clip.py index dbf10c5163..4b14f2493f 100644 --- a/openpype/hosts/resolve/plugins/create/create_shot_clip.py +++ b/openpype/hosts/resolve/plugins/create/create_shot_clip.py @@ -1,9 +1,12 @@ # from pprint import pformat -from openpype.hosts import resolve -from openpype.hosts.resolve.api import lib +from openpype.hosts.resolve.api import plugin, lib +from openpype.hosts.resolve.api.lib import ( + get_video_track_names, + create_bin, +) -class CreateShotClip(resolve.Creator): +class CreateShotClip(plugin.Creator): """Publishable clip""" label = "Create Publishable Clip" @@ -11,7 +14,7 @@ class CreateShotClip(resolve.Creator): icon = "film" defaults = ["Main"] - gui_tracks = resolve.get_video_track_names() + gui_tracks = get_video_track_names() gui_name = "OpenPype publish attributes creator" gui_info = "Define sequential rename and fill hierarchy data." gui_inputs = { @@ -250,7 +253,7 @@ class CreateShotClip(resolve.Creator): sq_markers = self.timeline.GetMarkers() # create media bin for compound clips (trackItems) - mp_folder = resolve.create_bin(self.timeline.GetName()) + mp_folder = create_bin(self.timeline.GetName()) kwargs = { "ui_inputs": widget.result, @@ -264,6 +267,6 @@ class CreateShotClip(resolve.Creator): self.rename_index = i self.log.info(track_item_data) # convert track item to timeline media pool item - track_item = resolve.PublishClip( + track_item = plugin.PublishClip( self, track_item_data, **kwargs).convert() track_item.SetClipColor(lib.publish_clip_color) diff --git a/openpype/hosts/resolve/plugins/load/load_clip.py b/openpype/hosts/resolve/plugins/load/load_clip.py index 190a5a7206..a0c78c182f 100644 --- a/openpype/hosts/resolve/plugins/load/load_clip.py +++ b/openpype/hosts/resolve/plugins/load/load_clip.py @@ -1,21 +1,22 @@ from copy import deepcopy -from importlib import reload from openpype.client import ( get_version_by_id, get_last_version_by_subset_id, ) -from openpype.hosts import resolve +# from openpype.hosts import resolve from openpype.pipeline import ( get_representation_path, legacy_io, ) from openpype.hosts.resolve.api import lib, plugin -reload(plugin) -reload(lib) +from openpype.hosts.resolve.api.pipeline import ( + containerise, + update_container, +) -class LoadClip(resolve.TimelineItemLoader): +class LoadClip(plugin.TimelineItemLoader): """Load a subset to timeline as clip Place clip to timeline on its asset origin timings collected @@ -46,7 +47,7 @@ class LoadClip(resolve.TimelineItemLoader): }) # load clip to timeline and get main variables - timeline_item = resolve.ClipLoader( + timeline_item = plugin.ClipLoader( self, context, **options).load() namespace = namespace or timeline_item.GetName() version = context['version'] @@ -80,7 +81,7 @@ class LoadClip(resolve.TimelineItemLoader): self.log.info("Loader done: `{}`".format(name)) - return resolve.containerise( + return containerise( timeline_item, name, namespace, context, self.__class__.__name__, @@ -98,7 +99,7 @@ class LoadClip(resolve.TimelineItemLoader): context.update({"representation": representation}) name = container['name'] namespace = container['namespace'] - timeline_item_data = resolve.get_pype_timeline_item_by_name(namespace) + timeline_item_data = lib.get_pype_timeline_item_by_name(namespace) timeline_item = timeline_item_data["clip"]["item"] project_name = legacy_io.active_project() version = get_version_by_id(project_name, representation["parent"]) @@ -109,7 +110,7 @@ class LoadClip(resolve.TimelineItemLoader): self.fname = get_representation_path(representation) context["version"] = {"data": version_data} - loader = resolve.ClipLoader(self, context) + loader = plugin.ClipLoader(self, context) timeline_item = loader.update(timeline_item) # add additional metadata from the version to imprint Avalon knob @@ -136,7 +137,7 @@ class LoadClip(resolve.TimelineItemLoader): # update color of clip regarding the version order self.set_item_color(timeline_item, version) - return resolve.update_container(timeline_item, data_imprint) + return update_container(timeline_item, data_imprint) @classmethod def set_item_color(cls, timeline_item, version): diff --git a/openpype/hosts/resolve/plugins/publish/extract_workfile.py b/openpype/hosts/resolve/plugins/publish/extract_workfile.py index e3d60465a2..ea8f19cd8c 100644 --- a/openpype/hosts/resolve/plugins/publish/extract_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/extract_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api import openpype.api -from openpype.hosts import resolve +from openpype.hosts.resolve.api.lib import get_project_manager class ExtractWorkfile(openpype.api.Extractor): @@ -29,7 +29,7 @@ class ExtractWorkfile(openpype.api.Extractor): os.path.join(staging_dir, drp_file_name)) # write out the drp workfile - resolve.get_project_manager().ExportProject( + get_project_manager().ExportProject( project.GetName(), drp_file_path) # create drp workfile representation diff --git a/openpype/hosts/resolve/plugins/publish/precollect_instances.py b/openpype/hosts/resolve/plugins/publish/precollect_instances.py index ee51998c0d..8ec169ad65 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_instances.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_instances.py @@ -1,9 +1,15 @@ -import pyblish -from openpype.hosts import resolve - -# # developer reload modules from pprint import pformat +import pyblish + +from openpype.hosts.resolve.api.lib import ( + get_current_timeline_items, + get_timeline_item_pype_tag, + publish_clip_color, + get_publish_attribute, + get_otio_clip_instance_data, +) + class PrecollectInstances(pyblish.api.ContextPlugin): """Collect all Track items selection.""" @@ -14,8 +20,8 @@ class PrecollectInstances(pyblish.api.ContextPlugin): def process(self, context): otio_timeline = context.data["otioTimeline"] - selected_timeline_items = resolve.get_current_timeline_items( - filter=True, selecting_color=resolve.publish_clip_color) + selected_timeline_items = get_current_timeline_items( + filter=True, selecting_color=publish_clip_color) self.log.info( "Processing enabled track items: {}".format( @@ -27,7 +33,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): timeline_item = timeline_item_data["clip"]["item"] # get pype tag data - tag_data = resolve.get_timeline_item_pype_tag(timeline_item) + tag_data = get_timeline_item_pype_tag(timeline_item) self.log.debug(f"__ tag_data: {pformat(tag_data)}") if not tag_data: @@ -67,7 +73,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "asset": asset, "item": timeline_item, "families": families, - "publish": resolve.get_publish_attribute(timeline_item), + "publish": get_publish_attribute(timeline_item), "fps": context.data["fps"], "handleStart": handle_start, "handleEnd": handle_end, @@ -75,7 +81,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): }) # otio clip data - otio_data = resolve.get_otio_clip_instance_data( + otio_data = get_otio_clip_instance_data( otio_timeline, timeline_item_data) or {} data.update(otio_data) @@ -134,7 +140,7 @@ class PrecollectInstances(pyblish.api.ContextPlugin): "asset": asset, "family": family, "families": [], - "publish": resolve.get_publish_attribute(timeline_item) + "publish": get_publish_attribute(timeline_item) }) context.create_instance(**data) diff --git a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py index 3a16b9c966..8f3917bece 100644 --- a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py +++ b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py @@ -6,10 +6,11 @@ from openpype.pipeline import install_host def main(env): - import openpype.hosts.resolve as bmdvr + from openpype.hosts.resolve.utils import setup + import openpype.hosts.resolve.api as bmdvr # Registers openpype's Global pyblish plugins install_host(bmdvr) - bmdvr.setup(env) + setup(env) if __name__ == "__main__": diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py index 89ade9238b..1087a7b7a0 100644 --- a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py +++ b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py @@ -2,13 +2,13 @@ import os import sys from openpype.pipeline import install_host -from openpype.api import Logger +from openpype.lib import Logger -log = Logger().get_logger(__name__) +log = Logger.get_logger(__name__) def main(env): - import openpype.hosts.resolve as bmdvr + import openpype.hosts.resolve.api as bmdvr # activate resolve from openpype install_host(bmdvr) diff --git a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py index 8433bd9172..92f2e43a72 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py +++ b/openpype/hosts/resolve/utility_scripts/tests/test_otio_as_edl.py @@ -6,8 +6,8 @@ import opentimelineio as otio from openpype.pipeline import install_host -from openpype.hosts.resolve import TestGUI -import openpype.hosts.resolve as bmdvr +import openpype.hosts.resolve.api as bmdvr +from openpype.hosts.resolve.api.testing_utils import TestGUI from openpype.hosts.resolve.otio import davinci_export as otio_export diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py index 477955d527..91a361ec08 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_create_timeline_item_from_path.py @@ -2,11 +2,16 @@ import os import sys -from openpype.pipeline import install_host -from openpype.hosts.resolve import TestGUI -import openpype.hosts.resolve as bmdvr import clique +from openpype.pipeline import install_host +from openpype.hosts.resolve.api.testing_utils import TestGUI +import openpype.hosts.resolve.api as bmdvr +from openpype.hosts.resolve.api.lib import ( + create_media_pool_item, + create_timeline_item, +) + class ThisTestGUI(TestGUI): extensions = [".exr", ".jpg", ".mov", ".png", ".mp4", ".ari", ".arx"] @@ -55,10 +60,10 @@ class ThisTestGUI(TestGUI): # skip if unwanted extension if ext not in self.extensions: return - media_pool_item = bmdvr.create_media_pool_item(fpath) + media_pool_item = create_media_pool_item(fpath) print(media_pool_item) - track_item = bmdvr.create_timeline_item(media_pool_item) + track_item = create_timeline_item(media_pool_item) print(track_item) diff --git a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py index 872d620162..2e83188bde 100644 --- a/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py +++ b/openpype/hosts/resolve/utility_scripts/tests/testing_load_media_pool_item.py @@ -1,13 +1,17 @@ #! python3 from openpype.pipeline import install_host -import openpype.hosts.resolve as bmdvr +from openpype.hosts.resolve import api as bmdvr +from openpype.hosts.resolve.api.lib import ( + create_media_pool_item, + create_timeline_item, +) def file_processing(fpath): - media_pool_item = bmdvr.create_media_pool_item(fpath) + media_pool_item = create_media_pool_item(fpath) print(media_pool_item) - track_item = bmdvr.create_timeline_item(media_pool_item) + track_item = create_timeline_item(media_pool_item) print(track_item) From a777238d83282e24c6238b92143b3a424ccde40d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 11 Aug 2022 19:11:49 +0200 Subject: [PATCH 093/122] fix handling of host name in error message --- openpype/host/host.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/openpype/host/host.py b/openpype/host/host.py index 48907e7ec7..9cdbb819e1 100644 --- a/openpype/host/host.py +++ b/openpype/host/host.py @@ -19,8 +19,15 @@ class MissingMethodsError(ValueError): joined_missing = ", ".join( ['"{}"'.format(item) for item in missing_methods] ) + if isinstance(host, HostBase): + host_name = host.name + else: + try: + host_name = host.__file__.replace("\\", "/").split("/")[-3] + except Exception: + host_name = str(host) message = ( - "Host \"{}\" miss methods {}".format(host.name, joined_missing) + "Host \"{}\" miss methods {}".format(host_name, joined_missing) ) super(MissingMethodsError, self).__init__(message) From 089cd3f9fa3587178c9fe73371b4470588b8467b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:00 +0200 Subject: [PATCH 094/122] added missing docstring for 'context_filters' argument --- openpype/client/entities.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/client/entities.py b/openpype/client/entities.py index c798c0ad6d..67ddb09ddb 100644 --- a/openpype/client/entities.py +++ b/openpype/client/entities.py @@ -1220,6 +1220,8 @@ def get_archived_representations( as filter. Filter ignored if 'None' is passed. version_ids (Iterable[str]): Subset ids used as parent filter. Filter ignored if 'None' is passed. + context_filters (Dict[str, List[str, re.Pattern]]): Filter by + representation context fields. names_by_version_ids (dict[ObjectId, List[str]]): Complex filtering using version ids and list of names under the version. fields (Iterable[str]): Fields that should be returned. All fields are From aefb992ce55145f94790bbaa5cdbf17136684e1e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:35 +0200 Subject: [PATCH 095/122] removed unused 'is_context' property --- .../workfile/abstract_template_loader.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index 1c8ede25e6..e2f9fdba0f 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -465,23 +465,6 @@ class AbstractPlaceholder: return self.data["loader"] - @property - def is_context(self): - """Check if is placeholder context type. - - context_asset: For loading current asset - linked_asset: For loading linked assets - - Question: - There seems to be more build options and this property is not used, - should be removed? - - Returns: - bool: true if placeholder is a context placeholder - """ - - return self.builder_type == "context_asset" - @property def is_valid(self): """Test validity of placeholder. From 32c2440e4a6d5d5ec3d54c2d1a44ffc5f0f81ae5 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 12 Aug 2022 13:55:55 +0200 Subject: [PATCH 096/122] fix docstring header --- openpype/pipeline/workfile/abstract_template_loader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/pipeline/workfile/abstract_template_loader.py b/openpype/pipeline/workfile/abstract_template_loader.py index e2f9fdba0f..05a98a1ddc 100644 --- a/openpype/pipeline/workfile/abstract_template_loader.py +++ b/openpype/pipeline/workfile/abstract_template_loader.py @@ -456,7 +456,7 @@ class AbstractPlaceholder: @property def loader_name(self): - """Return placeholder loader type. + """Return placeholder loader name. Returns: str: Loader name that will be used to load placeholder From 61e8d7e9f1fbffd91e268ef3ff721cc136395f27 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:14:01 +0200 Subject: [PATCH 097/122] use 'get_projects' instead of 'projects' method on AvalonMongoDB --- .../modules/kitsu/utils/update_zou_with_op.py | 8 +++- .../modules/sync_server/sync_server_module.py | 9 ++-- openpype/tools/launcher/models.py | 3 +- openpype/tools/libraryloader/app.py | 4 +- .../project_manager/project_manager/model.py | 7 +--- openpype/tools/sceneinventory/window.py | 6 +-- openpype/tools/utils/models.py | 41 ++++++++++--------- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/openpype/modules/kitsu/utils/update_zou_with_op.py b/openpype/modules/kitsu/utils/update_zou_with_op.py index 57d7094e95..da924aa5ee 100644 --- a/openpype/modules/kitsu/utils/update_zou_with_op.py +++ b/openpype/modules/kitsu/utils/update_zou_with_op.py @@ -6,7 +6,11 @@ from typing import List import gazu from pymongo import UpdateOne -from openpype.client import get_project, get_assets +from openpype.client import ( + get_projects, + get_project, + get_assets, +) from openpype.pipeline import AvalonMongoDB from openpype.api import get_project_settings from openpype.modules.kitsu.utils.credentials import validate_credentials @@ -37,7 +41,7 @@ def sync_zou(login: str, password: str): dbcon = AvalonMongoDB() dbcon.install() - op_projects = [p for p in dbcon.projects()] + op_projects = list(get_projects()) for project_doc in op_projects: sync_zou_from_op_project(project_doc["name"], dbcon, project_doc) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 8fdfab9c2e..c7f9484e55 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -6,7 +6,7 @@ import platform import copy from collections import deque, defaultdict - +from openpype.client import get_projects from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule from openpype.settings import ( @@ -913,7 +913,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): enabled_projects = [] if self.enabled: - for project in self.connection.projects(projection={"name": 1}): + for project in get_projects(fields=["name"]): project_name = project["name"] if self.is_project_enabled(project_name): enabled_projects.append(project_name) @@ -1242,10 +1242,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def _prepare_sync_project_settings(self, exclude_locals): sync_project_settings = {} system_sites = self.get_all_site_configs() - project_docs = self.connection.projects( - projection={"name": 1}, - only_active=True - ) + project_docs = get_projects(fields=["name"]) for project_doc in project_docs: project_name = project_doc["name"] sites = copy.deepcopy(system_sites) # get all configured sites diff --git a/openpype/tools/launcher/models.py b/openpype/tools/launcher/models.py index 3f899cc05e..6d40d21f96 100644 --- a/openpype/tools/launcher/models.py +++ b/openpype/tools/launcher/models.py @@ -10,6 +10,7 @@ from Qt import QtCore, QtGui import qtawesome from openpype.client import ( + get_projects, get_project, get_assets, ) @@ -527,7 +528,7 @@ class LauncherModel(QtCore.QObject): current_project = self.project_name project_names = set() project_docs_by_name = {} - for project_doc in self._dbcon.projects(only_active=True): + for project_doc in get_projects(): project_name = project_doc["name"] project_names.add(project_name) project_docs_by_name[project_name] = project_doc diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index 5f4d10d796..d2af1b7151 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -3,7 +3,7 @@ import sys from Qt import QtWidgets, QtCore, QtGui from openpype import style -from openpype.client import get_project +from openpype.client import get_projects, get_project from openpype.pipeline import AvalonMongoDB from openpype.tools.utils import lib as tools_lib from openpype.tools.loader.widgets import ( @@ -239,7 +239,7 @@ class LibraryLoaderWindow(QtWidgets.QDialog): def get_filtered_projects(self): projects = list() - for project in self.dbcon.projects(): + for project in get_projects(fields=["name", "data.library_project"]): is_library = project.get("data", {}).get("library_project", False) if ( (is_library and self.show_libraries) or diff --git a/openpype/tools/project_manager/project_manager/model.py b/openpype/tools/project_manager/project_manager/model.py index c5bde5aaec..3aaee75698 100644 --- a/openpype/tools/project_manager/project_manager/model.py +++ b/openpype/tools/project_manager/project_manager/model.py @@ -8,6 +8,7 @@ from pymongo import UpdateOne, DeleteOne from Qt import QtCore, QtGui from openpype.client import ( + get_projects, get_project, get_assets, get_asset_ids_with_subsets, @@ -54,12 +55,8 @@ class ProjectModel(QtGui.QStandardItemModel): self._items_by_name[None] = none_project new_project_items.append(none_project) - project_docs = self.dbcon.projects( - projection={"name": 1}, - only_active=True - ) project_names = set() - for project_doc in project_docs: + for project_doc in get_projects(fields=["name"]): project_name = project_doc.get("name") if not project_name: continue diff --git a/openpype/tools/sceneinventory/window.py b/openpype/tools/sceneinventory/window.py index 054c2a2daa..463280b71c 100644 --- a/openpype/tools/sceneinventory/window.py +++ b/openpype/tools/sceneinventory/window.py @@ -4,8 +4,9 @@ import sys from Qt import QtWidgets, QtCore import qtawesome -from openpype.pipeline import legacy_io from openpype import style +from openpype.client import get_projects +from openpype.pipeline import legacy_io from openpype.tools.utils.delegates import VersionDelegate from openpype.tools.utils.lib import ( qt_app_context, @@ -195,8 +196,7 @@ def show(root=None, debug=False, parent=None, items=None): if not os.environ.get("AVALON_PROJECT"): any_project = next( - project for project in legacy_io.projects() - if project.get("active", True) is not False + project for project in get_projects() ) project_name = any_project["name"] diff --git a/openpype/tools/utils/models.py b/openpype/tools/utils/models.py index 8991614fe1..1faccef4dd 100644 --- a/openpype/tools/utils/models.py +++ b/openpype/tools/utils/models.py @@ -3,6 +3,7 @@ import logging import Qt from Qt import QtCore, QtGui +from openpype.client import get_projects from .constants import ( PROJECT_IS_ACTIVE_ROLE, PROJECT_NAME_ROLE, @@ -296,29 +297,29 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item = item project_names = set() - if self.dbcon is not None: - for project_doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - project_name = project_doc["name"] - project_names.add(project_name) - if project_name in self._items_by_name: - item = self._items_by_name[project_name] - else: - item = QtGui.QStandardItem(project_name) + project_docs = get_projects( + inactive=not self._only_active, + fields=["name", "data.active"] + ) + for project_doc in project_docs: + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) - self._items_by_name[project_name] = item - new_items.append(item) + self._items_by_name[project_name] = item + new_items.append(item) - is_active = project_doc.get("data", {}).get("active", True) - item.setData(project_name, PROJECT_NAME_ROLE) - item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) - if not is_active: - font = item.font() - font.setItalic(True) - item.setFont(font) + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): From 260ef9999516d437ad399a0b746ff73632106314 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:15:42 +0200 Subject: [PATCH 098/122] removed unused code --- openpype/tools/utils/lib.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 2169cf8ef1..99d8c75ab4 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -443,10 +443,6 @@ class FamilyConfigCache: if profiles: # Make sure connection is installed # - accessing attribute which does not have auto-install - self.dbcon.install() - database = getattr(self.dbcon, "database", None) - if database is None: - database = self.dbcon._database asset_doc = get_asset_by_name( project_name, asset_name, fields=["data.tasks"] ) or {} From e86ea84da897f0ecd6608bdfff460c742a10042e Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:18:37 +0200 Subject: [PATCH 099/122] use 'get_projects' in standalone publisher --- openpype/tools/standalonepublish/widgets/widget_asset.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/tools/standalonepublish/widgets/widget_asset.py b/openpype/tools/standalonepublish/widgets/widget_asset.py index 73114f7960..77d756a606 100644 --- a/openpype/tools/standalonepublish/widgets/widget_asset.py +++ b/openpype/tools/standalonepublish/widgets/widget_asset.py @@ -3,6 +3,7 @@ from Qt import QtWidgets, QtCore import qtawesome from openpype.client import ( + get_projects, get_project, get_asset_by_id, ) @@ -291,9 +292,7 @@ class AssetWidget(QtWidgets.QWidget): def _set_projects(self): project_names = list() - for doc in self.dbcon.projects(projection={"name": 1}, - only_active=True): - + for doc in get_projects(fields=["name"]): project_name = doc.get("name") if project_name: project_names.append(project_name) @@ -320,8 +319,7 @@ class AssetWidget(QtWidgets.QWidget): def on_project_change(self): projects = list() - for project in self.dbcon.projects(projection={"name": 1}, - only_active=True): + for project in get_projects(fields=["name"]): projects.append(project['name']) project_name = self.combo_projects.currentText() if project_name in projects: From 68ca5898920d79c93bc51e699f319e5987019063 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 15 Aug 2022 19:18:50 +0200 Subject: [PATCH 100/122] use 'get_projects' in settings --- openpype/tools/settings/settings/widgets.py | 60 ++++++++------------- 1 file changed, 22 insertions(+), 38 deletions(-) diff --git a/openpype/tools/settings/settings/widgets.py b/openpype/tools/settings/settings/widgets.py index 88d923c16a..1a4a6877b0 100644 --- a/openpype/tools/settings/settings/widgets.py +++ b/openpype/tools/settings/settings/widgets.py @@ -3,6 +3,7 @@ import uuid from Qt import QtWidgets, QtCore, QtGui import qtawesome +from openpype.client import get_projects from openpype.pipeline import AvalonMongoDB from openpype.style import get_objected_colors from openpype.tools.utils.widgets import ImageButton @@ -783,8 +784,6 @@ class ProjectModel(QtGui.QStandardItemModel): self.setColumnCount(2) - self.dbcon = None - self._only_active = only_active self._default_item = None self._items_by_name = {} @@ -828,9 +827,6 @@ class ProjectModel(QtGui.QStandardItemModel): index = self.index(index.row(), 0, index.parent()) return super(ProjectModel, self).flags(index) - def set_dbcon(self, dbcon): - self.dbcon = dbcon - def refresh(self): # Change id of versions refresh self._version_refresh_id = uuid.uuid4() @@ -846,31 +842,30 @@ class ProjectModel(QtGui.QStandardItemModel): self._default_item.setData("", PROJECT_VERSION_ROLE) project_names = set() - if self.dbcon is not None: - for project_doc in self.dbcon.projects( - projection={"name": 1, "data.active": 1}, - only_active=self._only_active - ): - project_name = project_doc["name"] - project_names.add(project_name) - if project_name in self._items_by_name: - item = self._items_by_name[project_name] - else: - item = QtGui.QStandardItem(project_name) + for project_doc in get_projects( + inactive=not self._only_active, + fields=["name", "data.active"] + ): + project_name = project_doc["name"] + project_names.add(project_name) + if project_name in self._items_by_name: + item = self._items_by_name[project_name] + else: + item = QtGui.QStandardItem(project_name) - self._items_by_name[project_name] = item - new_items.append(item) + self._items_by_name[project_name] = item + new_items.append(item) - is_active = project_doc.get("data", {}).get("active", True) - item.setData(project_name, PROJECT_NAME_ROLE) - item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) - item.setData("", PROJECT_VERSION_ROLE) - item.setData(False, PROJECT_IS_SELECTED_ROLE) + is_active = project_doc.get("data", {}).get("active", True) + item.setData(project_name, PROJECT_NAME_ROLE) + item.setData(is_active, PROJECT_IS_ACTIVE_ROLE) + item.setData("", PROJECT_VERSION_ROLE) + item.setData(False, PROJECT_IS_SELECTED_ROLE) - if not is_active: - font = item.font() - font.setItalic(True) - item.setFont(font) + if not is_active: + font = item.font() + font.setItalic(True) + item.setFont(font) root_item = self.invisibleRootItem() for project_name in tuple(self._items_by_name.keys()): @@ -1067,8 +1062,6 @@ class ProjectListWidget(QtWidgets.QWidget): self.project_model = project_model self.inactive_chk = inactive_chk - self.dbcon = None - def set_entity(self, entity): self._entity = entity @@ -1211,15 +1204,6 @@ class ProjectListWidget(QtWidgets.QWidget): selected_project = index.data(PROJECT_NAME_ROLE) break - if not self.dbcon: - try: - self.dbcon = AvalonMongoDB() - self.dbcon.install() - except Exception: - self.dbcon = None - self.current_project = None - - self.project_model.set_dbcon(self.dbcon) self.project_model.refresh() self.project_proxy.sort(0) From 345a476159ed6d0f684f89316a488bba52347d93 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 11:55:09 +0200 Subject: [PATCH 101/122] prepared settings to be able change task status on creation --- .../defaults/project_settings/ftrack.json | 3 ++ .../schema_project_ftrack.json | 38 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 3e86581a03..9847e58cfa 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -434,6 +434,9 @@ "enabled": false, "custom_attribute_keys": [] }, + "IntegrateHierarchyToFtrack": { + "create_task_status_profiles": [] + }, "IntegrateFtrackNote": { "enabled": true, "note_template": "{intent}: {comment}", diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index c06bec0f58..3f472c6c6a 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -841,6 +841,44 @@ } ] }, + { + "type": "dict", + "key": "IntegrateHierarchyToFtrack", + "label": "Integrate Hierarchy to ftrack", + "is_group": true, + "collapsible": true, + "children": [ + { + "type": "label", + "label": "Set task status on new task creation. Ftrack's default status is used otherwise." + }, + { + "type": "list", + "key": "create_task_status_profiles", + "object_type": { + "type": "dict", + "children": [ + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "type": "text", + "key": "status_name", + "label": "Status name" + } + ] + } + } + ] + }, { "type": "dict", "collapsible": true, From 088a2d2003e111769084821ac1e2c2ece3cf2e35 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 11:55:23 +0200 Subject: [PATCH 102/122] use task status profiles to change task status id on creation --- .../publish/integrate_hierarchy_ftrack.py | 62 ++++++++++++++----- 1 file changed, 45 insertions(+), 17 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py index b8855ee2bd..8d39baa8d7 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_hierarchy_ftrack.py @@ -1,9 +1,12 @@ import sys import collections import six -import pyblish.api from copy import deepcopy + +import pyblish.api + from openpype.client import get_asset_by_id +from openpype.lib import filter_profiles # Copy of constant `openpype_modules.ftrack.lib.avalon_sync.CUST_ATTR_AUTO_SYNC` @@ -73,6 +76,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): "traypublisher" ] optional = False + create_task_status_profiles = [] def process(self, context): self.context = context @@ -82,14 +86,16 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): hierarchy_context = self._get_active_assets(context) self.log.debug("__ hierarchy_context: {}".format(hierarchy_context)) - self.session = self.context.data["ftrackSession"] + session = self.context.data["ftrackSession"] project_name = self.context.data["projectEntity"]["name"] query = 'Project where full_name is "{}"'.format(project_name) - project = self.session.query(query).one() - auto_sync_state = project[ - "custom_attributes"][CUST_ATTR_AUTO_SYNC] + project = session.query(query).one() + auto_sync_state = project["custom_attributes"][CUST_ATTR_AUTO_SYNC] - self.ft_project = None + self.session = session + self.ft_project = project + self.task_types = self.get_all_task_types(project) + self.task_statuses = self.get_task_statuses(project) # disable termporarily ftrack project's autosyncing if auto_sync_state: @@ -121,10 +127,7 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.log.debug(entity_type) if entity_type.lower() == 'project': - query = 'Project where full_name is "{}"'.format(entity_name) - entity = self.session.query(query).one() - self.ft_project = entity - self.task_types = self.get_all_task_types(entity) + entity = self.ft_project elif self.ft_project is None or parent is None: raise AssertionError( @@ -217,13 +220,6 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): task_type=task_type, parent=entity ) - try: - self.session.commit() - except Exception: - tp, value, tb = sys.exc_info() - self.session.rollback() - self.session._configure_locations() - six.reraise(tp, value, tb) # Incoming links. self.create_links(project_name, entity_data, entity) @@ -303,7 +299,37 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): return tasks + def get_task_statuses(self, project_entity): + project_schema = project_entity["project_schema"] + task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + return { + status["id"]: status + for status in task_workflow_statuses + } + def create_task(self, name, task_type, parent): + filter_data = { + "task_names": name, + "task_types": task_type + } + profile = filter_profiles( + self.create_task_status_profiles, + filter_data + ) + status_id = None + if profile: + status_name = profile["status_name"] + status_name_low = status_name.lower() + for _status_id, status in self.task_statuses.items(): + if status["name"].lower() == status_name_low: + status_id = _status_id + break + + if status_id is None: + self.log.warning( + "Task status \"{}\" was not found".format(status_name) + ) + task = self.session.create('Task', { 'name': name, 'parent': parent @@ -312,6 +338,8 @@ class IntegrateHierarchyToFtrack(pyblish.api.ContextPlugin): self.log.info(task_type) self.log.info(self.task_types) task['type'] = self.task_types[task_type] + if status_id is not None: + task["status_id"] = status_id try: self.session.commit() From e0c7ba861733e0cf4ec9087fdadcfe6b0d729aea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 17:53:16 +0200 Subject: [PATCH 103/122] added new plugin which change task status for instances if they are rendered on farm --- .../publish/integrate_ftrack_farm_status.py | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py new file mode 100644 index 0000000000..ecf258a870 --- /dev/null +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -0,0 +1,129 @@ +import pyblish.api +from openpype.lib import profiles_filtering + + +class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): + """Change task status when should be published on farm. + + Instance which has set "farm" key in data to 'True' is considered as will + be rendered on farm thus it's status should be changed. + """ + + order = pyblish.api.IntegratorOrder + 0.48 + label = "Integrate Ftrack Component" + families = ["ftrack"] + + farm_status_profiles = [] + + def process(self, context): + # Quick end + if not self.farm_status_profiles: + project_name = context.data["projectName"] + self.log.info(( + "Status profiles are not filled for project \"{}\". Skipping" + ).format(project_name)) + return + + filtered_instances = self.filter_instances(context) + instances_with_status_names = self.get_instances_with_statuse_names( + context, filtered_instances + ) + if instances_with_status_names: + self.fill_statuses(context, instances_with_status_names) + + def filter_instances(self, context): + filtered_instances = [] + for instance in context: + subset_name = instance.data["subset"] + msg_start = "SKipping instance {}.".format(subset_name) + if not instance.data.get("farm"): + self.log.debug( + "{} Won't be rendered on farm.".format(msg_start) + ) + continue + + task_entity = instance.data.get("ftrackTask") + if not task_entity: + self.log.debug( + "{} Does not have filled task".format(msg_start) + ) + continue + + filtered_instances.append(instance) + return filtered_instances + + def get_instances_with_statuse_names(self, context, instances): + instances_with_status_names = [] + for instance in instances: + family = instance.data["family"] + subset_name = instance.data["subset"] + task_entity = instance.data["ftrackTask"] + host_name = context.data["hostName"] + task_name = task_entity["name"] + task_type = task_entity["type"]["name"] + status_profile = profiles_filtering( + self.farm_status_profiles, + { + "hosts": host_name, + "task_types": task_type, + "task_names": task_name, + "families": family, + "subsets": subset_name, + }, + logger=self.log + ) + if not status_profile: + # There already is log in 'profiles_filtering' + continue + + status_name = status_profile["status_name"] + if status_name: + instances_with_status_names.append((instance, status_name)) + return instances_with_status_names + + def fill_statuses(self, context, instances_with_status_names): + # Prepare available task statuses on the project + project_name = context.data["projectName"] + session = context.data["ftrackSession"] + project_entity = session.query(( + "select project_schema from Project where full_name is \"{}\"" + ).format(project_name)).one() + project_schema = project_entity["project_schema"] + task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + + # Keep track if anything has changed + status_changed = False + found_status_id_by_status_name = {} + for item in instances_with_status_names: + instance, status_name = item + + status_name_low = status_name.lower() + status_id = found_status_id_by_status_name.get(status_name_low) + + if status_id is None: + # Skip if status name was already tried to be found + if status_name_low in found_status_id_by_status_name: + continue + + for status in task_workflow_statuses: + if status["name"].lower() == status_name_low: + status_id = status["id"] + break + + # Store the result to be reused in following instances + found_status_id_by_status_name[status_name_low] = status_id + + if status_id is None: + self.log.warning(( + "Status \"{}\" is not available on project \"{}\"" + ).format(status_name, project_name)) + continue + + # Change task status id + task_entity = instance.data["ftrackTask"] + if status_id != task_entity["status_id"]: + task_entity["status_id"] = status_id + status_changed = True + + if status_changed: + session.commit() From 41dd9e84f574663aef840596fa4e4c8a37a6a49b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Tue, 16 Aug 2022 17:53:27 +0200 Subject: [PATCH 104/122] added settings schema for new plugin --- .../defaults/project_settings/ftrack.json | 3 + .../schema_project_ftrack.json | 60 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 3e86581a03..610c85d232 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -489,6 +489,9 @@ }, "keep_first_subset_name_for_review": true, "asset_versions_status_profiles": [] + }, + "IntegrateFtrackFarmStatus": { + "farm_status_profiles": [] } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index c06bec0f58..a821b1de76 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1003,6 +1003,66 @@ } } ] + }, + { + "type": "dict", + "key": "IntegrateFtrackFarmStatus", + "label": "Integrate Ftrack Farm Status", + "children": [ + { + "type": "label", + "label": "Change status of task when it's subset is rendered on farm" + }, + { + "type": "list", + "collapsible": true, + "key": "farm_status_profiles", + "label": "Farm status profiles", + "use_label_wrap": true, + "object_type": { + "type": "dict", + "children": [ + { + "key": "hosts", + "label": "Host names", + "type": "hosts-enum", + "multiselection": true + }, + { + "key": "task_types", + "label": "Task types", + "type": "task-types-enum" + }, + { + "key": "task_names", + "label": "Task names", + "type": "list", + "object_type": "text" + }, + { + "key": "families", + "label": "Families", + "type": "list", + "object_type": "text" + }, + { + "key": "subsets", + "label": "Subset names", + "type": "list", + "object_type": "text" + }, + { + "type": "separator" + }, + { + "key": "status_name", + "label": "Status name", + "type": "text" + } + ] + } + } + ] } ] } From b36b8ebee0ae424ab1189bc3d9629ef672097bb4 Mon Sep 17 00:00:00 2001 From: OpenPype Date: Wed, 17 Aug 2022 04:12:09 +0000 Subject: [PATCH 105/122] [Automated] Bump version --- CHANGELOG.md | 19 ++++++++----------- openpype/version.py | 2 +- pyproject.toml | 2 +- 3 files changed, 10 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2adb4ac154..80673e9f8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,15 @@ # Changelog -## [3.13.1-nightly.2](https://github.com/pypeclub/OpenPype/tree/HEAD) +## [3.13.1-nightly.3](https://github.com/pypeclub/OpenPype/tree/HEAD) [Full Changelog](https://github.com/pypeclub/OpenPype/compare/3.13.0...HEAD) **🐛 Bug fixes** +- General: Extract Review can scale with pixel aspect ratio [\#3644](https://github.com/pypeclub/OpenPype/pull/3644) +- Maya: Refactor moved usage of CreateRender settings [\#3643](https://github.com/pypeclub/OpenPype/pull/3643) - General: Hero version representations have full context [\#3638](https://github.com/pypeclub/OpenPype/pull/3638) +- Nuke: color settings for render write node is working now [\#3632](https://github.com/pypeclub/OpenPype/pull/3632) - Maya: FBX support for update in reference loader [\#3631](https://github.com/pypeclub/OpenPype/pull/3631) **🔀 Refactored code** @@ -16,7 +19,10 @@ **Merged pull requests:** +- Deadline: Global job pre load is not Pype 2 compatible [\#3666](https://github.com/pypeclub/OpenPype/pull/3666) +- Maya: Remove unused get current renderer logic [\#3645](https://github.com/pypeclub/OpenPype/pull/3645) - Kitsu|Fix: Movie project type fails & first loop children names [\#3636](https://github.com/pypeclub/OpenPype/pull/3636) +- fix the bug of failing to extract look when UDIMs format used in AiImage [\#3628](https://github.com/pypeclub/OpenPype/pull/3628) ## [3.13.0](https://github.com/pypeclub/OpenPype/tree/3.13.0) (2022-08-09) @@ -55,7 +61,6 @@ - General: Update imports in start script [\#3579](https://github.com/pypeclub/OpenPype/pull/3579) - Nuke: render family integration consistency [\#3576](https://github.com/pypeclub/OpenPype/pull/3576) - Ftrack: Handle missing published path in integrator [\#3570](https://github.com/pypeclub/OpenPype/pull/3570) -- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - Nuke: publish existing frames with slate with correct range [\#3555](https://github.com/pypeclub/OpenPype/pull/3555) **🔀 Refactored code** @@ -85,11 +90,10 @@ - General: Global thumbnail extractor is ready for more cases [\#3561](https://github.com/pypeclub/OpenPype/pull/3561) - Maya: add additional validators to Settings [\#3540](https://github.com/pypeclub/OpenPype/pull/3540) -- General: Interactive console in cli [\#3526](https://github.com/pypeclub/OpenPype/pull/3526) -- Ftrack: Automatic daily review session creation can define trigger hour [\#3516](https://github.com/pypeclub/OpenPype/pull/3516) **🐛 Bug fixes** +- Maya: fix Review image plane attribute [\#3569](https://github.com/pypeclub/OpenPype/pull/3569) - Maya: Fix animated attributes \(ie. overscan\) on loaded cameras breaking review publishing. [\#3562](https://github.com/pypeclub/OpenPype/pull/3562) - NewPublisher: Python 2 compatible html escape [\#3559](https://github.com/pypeclub/OpenPype/pull/3559) - Remove invalid submodules from `/vendor` [\#3557](https://github.com/pypeclub/OpenPype/pull/3557) @@ -98,12 +102,6 @@ - Module interfaces: Fix import error [\#3547](https://github.com/pypeclub/OpenPype/pull/3547) - Workfiles tool: Show of tool and it's flags [\#3539](https://github.com/pypeclub/OpenPype/pull/3539) - General: Create workfile documents works again [\#3538](https://github.com/pypeclub/OpenPype/pull/3538) -- Additional fixes for powershell scripts [\#3525](https://github.com/pypeclub/OpenPype/pull/3525) -- Maya: Added wrapper around cmds.setAttr [\#3523](https://github.com/pypeclub/OpenPype/pull/3523) -- Nuke: double slate [\#3521](https://github.com/pypeclub/OpenPype/pull/3521) -- General: Fix hash of centos oiio archive [\#3519](https://github.com/pypeclub/OpenPype/pull/3519) -- Maya: Renderman display output fix [\#3514](https://github.com/pypeclub/OpenPype/pull/3514) -- TrayPublisher: Simple creation enhancements and fixes [\#3513](https://github.com/pypeclub/OpenPype/pull/3513) **🔀 Refactored code** @@ -112,7 +110,6 @@ - Refactor Integrate Asset [\#3530](https://github.com/pypeclub/OpenPype/pull/3530) - General: Client docstrings cleanup [\#3529](https://github.com/pypeclub/OpenPype/pull/3529) - General: Move load related functions into pipeline [\#3527](https://github.com/pypeclub/OpenPype/pull/3527) -- General: Get current context document functions [\#3522](https://github.com/pypeclub/OpenPype/pull/3522) **Merged pull requests:** diff --git a/openpype/version.py b/openpype/version.py index 6ff5dfb7b5..9ae52e8370 100644 --- a/openpype/version.py +++ b/openpype/version.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- """Package declaring Pype version.""" -__version__ = "3.13.1-nightly.2" +__version__ = "3.13.1-nightly.3" diff --git a/pyproject.toml b/pyproject.toml index 9cbdc295ff..83ccf233d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "OpenPype" -version = "3.13.1-nightly.2" # OpenPype +version = "3.13.1-nightly.3" # OpenPype description = "Open VFX and Animation pipeline with support." authors = ["OpenPype Team "] license = "MIT License" From ccaef43535dd0d80c3184a325f51bfaea8409d75 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 10:32:11 +0200 Subject: [PATCH 106/122] changed description label --- .../entities/schemas/projects_schema/schema_project_ftrack.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index a821b1de76..1967a1150f 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1011,7 +1011,7 @@ "children": [ { "type": "label", - "label": "Change status of task when it's subset is rendered on farm" + "label": "Change status of task when it's subset is submitted to farm" }, { "type": "list", From 2f3e6a73e3f8d130fc639cd1c5c1429e4957ea2a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 10:48:50 +0200 Subject: [PATCH 107/122] Change label of plugin --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index ecf258a870..f725de3144 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -10,7 +10,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): """ order = pyblish.api.IntegratorOrder + 0.48 - label = "Integrate Ftrack Component" + label = "Integrate Ftrack Farm Status" families = ["ftrack"] farm_status_profiles = [] @@ -35,7 +35,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): filtered_instances = [] for instance in context: subset_name = instance.data["subset"] - msg_start = "SKipping instance {}.".format(subset_name) + msg_start = "Skipping instance {}.".format(subset_name) if not instance.data.get("farm"): self.log.debug( "{} Won't be rendered on farm.".format(msg_start) From 7095bff502f13498bb1dd7a7a173693bf43e72dd Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:04:58 +0200 Subject: [PATCH 108/122] set "farm" to true in maya render colletor --- openpype/hosts/maya/plugins/publish/collect_render.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index c3e6c98020..0d45ad4f9e 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -354,6 +354,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance = context.create_instance(expected_layer_name) instance.data["label"] = label + instance.data["farm"] = True instance.data.update(data) self.log.debug("data: {}".format(json.dumps(data, indent=4))) From 58f19f15f4a2c3d4ad6d0dd71089c0357904dcd9 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:05:04 +0200 Subject: [PATCH 109/122] skip disabled instances --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index f725de3144..fcbe71e0ac 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -34,6 +34,9 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): def filter_instances(self, context): filtered_instances = [] for instance in context: + # Skip disabled instances + if instance.data.get("publish") is False: + continue subset_name = instance.data["subset"] msg_start = "Skipping instance {}.".format(subset_name) if not instance.data.get("farm"): From 346e3b8300e01ac8b3ab4e2c52a7d0c25a169d33 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:11:39 +0200 Subject: [PATCH 110/122] removed families filter --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index fcbe71e0ac..24f784f83d 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -11,7 +11,6 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): order = pyblish.api.IntegratorOrder + 0.48 label = "Integrate Ftrack Farm Status" - families = ["ftrack"] farm_status_profiles = [] From 95c19cc412ecbfd0caf42e06ada91640e8da5885 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 11:22:56 +0200 Subject: [PATCH 111/122] fill context entities in all instances --- .../plugins/publish/collect_ftrack_api.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 14da188150..99a555014e 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -105,11 +105,17 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): context.data["ftrackEntity"] = asset_entity context.data["ftrackTask"] = task_entity - self.per_instance_process(context, asset_name, task_name) + self.per_instance_process(context, asset_entity, task_entity) def per_instance_process( - self, context, context_asset_name, context_task_name + self, context, context_asset_entity, context_task_entity ): + context_task_name = None + context_asset_name = None + if context_asset_entity: + context_asset_name = context_asset_entity["name"] + if context_task_entity: + context_task_name = context_task_entity["name"] instance_by_asset_and_task = {} for instance in context: self.log.debug( @@ -120,6 +126,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): if not instance_asset_name and not instance_task_name: self.log.debug("Instance does not have set context keys.") + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue elif instance_asset_name and instance_task_name: @@ -131,6 +139,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context is same as in publish context." " Asset: {} | Task: {}" ).format(context_asset_name, context_task_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue asset_name = instance_asset_name task_name = instance_task_name @@ -141,6 +151,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context task is same as in publish" " context. Task: {}" ).format(context_task_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = context_task_entity continue asset_name = context_asset_name @@ -152,6 +164,8 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): "Instance's context asset is same as in publish" " context. Asset: {}" ).format(context_asset_name)) + instance.data["ftrackEntity"] = context_asset_entity + instance.data["ftrackTask"] = None continue # Do not use context's task name From 51c27f28c0791633819f935e230a179ea20ff00b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 12:20:06 +0200 Subject: [PATCH 112/122] added ability to add additional metadata to components --- .../publish/integrate_ftrack_instances.py | 134 +++++++++++------- 1 file changed, 85 insertions(+), 49 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index a1e5922730..3f0cc176a2 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -3,6 +3,7 @@ import json import copy import pyblish.api +from openpype.lib.openpype_version import get_openpype_version from openpype.lib.transcoding import ( get_ffprobe_streams, convert_ffprobe_fps_to_float, @@ -20,6 +21,17 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): label = "Integrate Ftrack Component" families = ["ftrack"] + metadata_keys_to_label = { + "openpype_version": "OpenPype version", + "frame_start": "Frame start", + "frame_end": "Frame end", + "duration": "Duration", + "width": "Resolution width", + "height": "Resolution height", + "fps": "FPS", + "code": "Codec" + } + family_mapping = { "camera": "cam", "look": "look", @@ -43,6 +55,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): } keep_first_subset_name_for_review = True asset_versions_status_profiles = {} + additional_metadata_keys = [] def process(self, instance): self.log.debug("instance {}".format(instance)) @@ -105,7 +118,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "component_data": None, "component_path": None, "component_location": None, - "component_location_name": None + "component_location_name": None, + "additional_data": {} } # Filter types of representations @@ -152,6 +166,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "name": "thumbnail" } thumbnail_item["thumbnail"] = True + # Create copy of item before setting location src_components_to_add.append(copy.deepcopy(thumbnail_item)) # Create copy of first thumbnail @@ -248,19 +263,15 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): first_thumbnail_component[ "asset_data"]["name"] = extended_asset_name - component_meta = self._prepare_component_metadata( - instance, repre, repre_path, True - ) - # Change location review_item["component_path"] = repre_path # Change component data review_item["component_data"] = { # Default component name is "main". "name": "ftrackreview-mp4", - "metadata": { - "ftr_meta": json.dumps(component_meta) - } + "metadata": self._prepare_component_metadata( + instance, repre, repre_path, True + ) } if is_first_review_repre: @@ -302,13 +313,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): component_data = copy_src_item["component_data"] component_name = component_data["name"] component_data["name"] = component_name + "_src" - component_meta = self._prepare_component_metadata( + component_data["metadata"] = self._prepare_component_metadata( instance, repre, copy_src_item["component_path"], False ) - if component_meta: - component_data["metadata"] = { - "ftr_meta": json.dumps(component_meta) - } component_list.append(copy_src_item) # Add others representations as component @@ -326,16 +333,12 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ): other_item["asset_data"]["name"] = extended_asset_name - component_meta = self._prepare_component_metadata( - instance, repre, published_path, False - ) component_data = { - "name": repre["name"] + "name": repre["name"], + "metadata": self._prepare_component_metadata( + instance, repre, published_path, False + ) } - if component_meta: - component_data["metadata"] = { - "ftr_meta": json.dumps(component_meta) - } other_item["component_data"] = component_data other_item["component_location_name"] = unmanaged_location_name other_item["component_path"] = published_path @@ -354,6 +357,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): )) instance.data["ftrackComponentsList"] = component_list + def _collect_additional_metadata(self, streams): + pass + def _get_repre_path(self, instance, repre, only_published): """Get representation path that can be used for integration. @@ -423,6 +429,11 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): def _prepare_component_metadata( self, instance, repre, component_path, is_review ): + metadata = {} + if "openpype_version" in self.additional_metadata_keys: + label = self.metadata_keys_to_label["openpype_version"] + metadata[label] = get_openpype_version() + extension = os.path.splitext(component_path)[-1] streams = [] try: @@ -442,13 +453,23 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): # - exr is special case which can have issues with reading through # ffmpegh but we want to set fps for it if not video_streams and extension not in [".exr"]: - return {} + return metadata stream_width = None stream_height = None stream_fps = None frame_out = None + codec_label = None for video_stream in video_streams: + codec_label = video_stream.get("codec_long_name") + if not codec_label: + codec_label = video_stream.get("codec") + + if codec_label: + pix_fmt = video_stream.get("pix_fmt") + if pix_fmt: + codec_label += " ({})".format(pix_fmt) + tmp_width = video_stream.get("width") tmp_height = video_stream.get("height") if tmp_width and tmp_height: @@ -456,8 +477,8 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): stream_height = tmp_height input_framerate = video_stream.get("r_frame_rate") - duration = video_stream.get("duration") - if input_framerate is None or duration is None: + stream_duration = video_stream.get("duration") + if input_framerate is None or stream_duration is None: continue try: stream_fps = convert_ffprobe_fps_to_float( @@ -473,9 +494,9 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): stream_height = tmp_height self.log.debug("FPS from stream is {} and duration is {}".format( - input_framerate, duration + input_framerate, stream_duration )) - frame_out = float(duration) * stream_fps + frame_out = float(stream_duration) * stream_fps break # Prepare FPS @@ -483,43 +504,58 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): if instance_fps is None: instance_fps = instance.context.data["fps"] - if not is_review: - output = {} - fps = stream_fps or instance_fps - if fps: - output["frameRate"] = fps - - if stream_width and stream_height: - output["width"] = int(stream_width) - output["height"] = int(stream_height) - return output - - frame_start = repre.get("frameStartFtrack") - frame_end = repre.get("frameEndFtrack") - if frame_start is None or frame_end is None: - frame_start = instance.data["frameStart"] - frame_end = instance.data["frameEnd"] - - fps = None repre_fps = repre.get("fps") if repre_fps is not None: repre_fps = float(repre_fps) fps = stream_fps or repre_fps or instance_fps + # Prepare frame ranges + frame_start = repre.get("frameStartFtrack") + frame_end = repre.get("frameEndFtrack") + if frame_start is None or frame_end is None: + frame_start = instance.data["frameStart"] + frame_end = instance.data["frameEnd"] + duration = (frame_end - frame_start) + 1 + + for key, value in [ + ("fps", fps), + ("frame_start", frame_start), + ("frame_end", frame_end), + ("duration", duration), + ("width", stream_width), + ("height", stream_height), + ("fps", fps), + ("code", codec_label) + ]: + if not value or key not in self.additional_metadata_keys: + continue + label = self.metadata_keys_to_label[key] + metadata[label] = value + + if not is_review: + ftr_meta = {} + if fps: + ftr_meta["frameRate"] = fps + + if stream_width and stream_height: + ftr_meta["width"] = int(stream_width) + ftr_meta["height"] = int(stream_height) + metadata["ftr_meta"] = json.dumps(ftr_meta) + return metadata + # Frame end of uploaded video file should be duration in frames # - frame start is always 0 # - frame end is duration in frames if not frame_out: - frame_out = frame_end - frame_start + 1 + frame_out = duration # Ftrack documentation says that it is required to have # 'width' and 'height' in review component. But with those values # review video does not play. - component_meta = { + metadata["ftr_meta"] = json.dumps({ "frameIn": 0, "frameOut": frame_out, "frameRate": float(fps) - } - - return component_meta + }) + return metadata From b66c8088c3c9fcde06cdcf6cb837c1deb2c5cc1b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 12:24:01 +0200 Subject: [PATCH 113/122] added settings for 'additional_metadata_keys' --- .../defaults/project_settings/ftrack.json | 3 ++- .../projects_schema/schema_project_ftrack.json | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 9847e58cfa..952657251c 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -491,7 +491,8 @@ "usd": "usd" }, "keep_first_subset_name_for_review": true, - "asset_versions_status_profiles": [] + "asset_versions_status_profiles": [], + "additional_metadata_keys": [] } } } \ No newline at end of file diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json index 3f472c6c6a..1a63e589b2 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_ftrack.json @@ -1039,6 +1039,22 @@ } ] } + }, + { + "key": "additional_metadata_keys", + "label": "Additional metadata keys on components", + "type": "enum", + "multiselection": true, + "enum_items": [ + {"openpype_version": "OpenPype version"}, + {"frame_start": "Frame start"}, + {"frame_end": "Frame end"}, + {"duration": "Duration"}, + {"width": "Resolution width"}, + {"height": "Resolution height"}, + {"fps": "FPS"}, + {"code": "Codec"} + ] } ] } From 05f1b732b6edd1732139350528ad614095da5b70 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:22:34 +0200 Subject: [PATCH 114/122] fill context task entity in collect ftrack api --- openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py index 99a555014e..e13b7e65cd 100644 --- a/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py +++ b/openpype/modules/ftrack/plugins/publish/collect_ftrack_api.py @@ -165,7 +165,7 @@ class CollectFtrackApi(pyblish.api.ContextPlugin): " context. Asset: {}" ).format(context_asset_name)) instance.data["ftrackEntity"] = context_asset_entity - instance.data["ftrackTask"] = None + instance.data["ftrackTask"] = context_task_entity continue # Do not use context's task name From 4dba68c5bdade98048dd1ca15d7f03ac004dcf28 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:30:14 +0200 Subject: [PATCH 115/122] fix function import and call --- .../ftrack/plugins/publish/integrate_ftrack_farm_status.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 24f784f83d..0a7ad0b532 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -1,5 +1,5 @@ import pyblish.api -from openpype.lib import profiles_filtering +from openpype.lib import filter_profiles class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): @@ -63,7 +63,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): host_name = context.data["hostName"] task_name = task_entity["name"] task_type = task_entity["type"]["name"] - status_profile = profiles_filtering( + status_profile = filter_profiles( self.farm_status_profiles, { "hosts": host_name, @@ -75,7 +75,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): logger=self.log ) if not status_profile: - # There already is log in 'profiles_filtering' + # There already is log in 'filter_profiles' continue status_name = status_profile["status_name"] From cb34f4619e54ae887bc1ea38a0e1ec106d228167 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:30:20 +0200 Subject: [PATCH 116/122] log availabl status names --- .../plugins/publish/integrate_ftrack_farm_status.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 0a7ad0b532..8bebfd8485 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -93,6 +93,10 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_schema = project_entity["project_schema"] task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + joined_status_names = ", ".join({ + '"{}"'.format(status["name"]) + for status in task_workflow_statuses + }) # Keep track if anything has changed status_changed = False found_status_id_by_status_name = {} @@ -117,8 +121,9 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_id is None: self.log.warning(( - "Status \"{}\" is not available on project \"{}\"" - ).format(status_name, project_name)) + "Status \"{}\" is not available on project \"{}\"." + " Available statuses are {}" + ).format(status_name, project_name, joined_status_names)) continue # Change task status id From bc3aa4b1609e067e7b4a31a9874e8a415bcfcc71 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 13:54:06 +0200 Subject: [PATCH 117/122] fix getting of task statuses --- .../plugins/publish/integrate_ftrack_farm_status.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 8bebfd8485..658df70895 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -90,12 +90,17 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_entity = session.query(( "select project_schema from Project where full_name is \"{}\"" ).format(project_name)).one() + task_type = session.query( + "select id from ObjectType where name is \"Task\"" + ).first() project_schema = project_entity["project_schema"] - task_workflow_statuses = project_schema["_task_workflow"]["statuses"] + task_statuses = project_schema.get_statuses( + "Task", task_type["id"] + ) joined_status_names = ", ".join({ '"{}"'.format(status["name"]) - for status in task_workflow_statuses + for status in task_statuses }) # Keep track if anything has changed status_changed = False @@ -111,7 +116,7 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_name_low in found_status_id_by_status_name: continue - for status in task_workflow_statuses: + for status in task_statuses: if status["name"].lower() == status_name_low: status_id = status["id"] break From 671cf183fd73629e7a140784e518bc7718fa5431 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 14:18:13 +0200 Subject: [PATCH 118/122] fix statuses lookup by task type --- .../publish/integrate_ftrack_farm_status.py | 62 +++++++++---------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index 658df70895..c5fc3dd68f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -90,49 +90,49 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): project_entity = session.query(( "select project_schema from Project where full_name is \"{}\"" ).format(project_name)).one() - task_type = session.query( - "select id from ObjectType where name is \"Task\"" - ).first() project_schema = project_entity["project_schema"] - task_statuses = project_schema.get_statuses( - "Task", task_type["id"] - ) - joined_status_names = ", ".join({ - '"{}"'.format(status["name"]) - for status in task_statuses - }) + task_type_ids = set() + for item in instances_with_status_names: + instance, _ = item + task_entity = instance.data["ftrackTask"] + task_type_ids.add(task_entity["type"]["id"]) + + task_statuses_by_type_id = { + task_type_id: project_schema.get_statuses("Task", task_type_id) + for task_type_id in task_type_ids + } + # Keep track if anything has changed + skipped_status_names = set() status_changed = False - found_status_id_by_status_name = {} for item in instances_with_status_names: instance, status_name = item - + task_entity = instance.data["ftrackTask"] + task_statuses = task_statuses_by_type_id[task_entity["type"]["id"]] status_name_low = status_name.lower() - status_id = found_status_id_by_status_name.get(status_name_low) + + status_id = None + # Skip if status name was already tried to be found + for status in task_statuses: + if status["name"].lower() == status_name_low: + status_id = status["id"] + break if status_id is None: - # Skip if status name was already tried to be found - if status_name_low in found_status_id_by_status_name: - continue - - for status in task_statuses: - if status["name"].lower() == status_name_low: - status_id = status["id"] - break - - # Store the result to be reused in following instances - found_status_id_by_status_name[status_name_low] = status_id - - if status_id is None: - self.log.warning(( - "Status \"{}\" is not available on project \"{}\"." - " Available statuses are {}" - ).format(status_name, project_name, joined_status_names)) + if status_name_low not in skipped_status_names: + skipped_status_names.add(status_name_low) + joined_status_names = ", ".join({ + '"{}"'.format(status["name"]) + for status in task_statuses + }) + self.log.warning(( + "Status \"{}\" is not available on project \"{}\"." + " Available statuses are {}" + ).format(status_name, project_name, joined_status_names)) continue # Change task status id - task_entity = instance.data["ftrackTask"] if status_id != task_entity["status_id"]: task_entity["status_id"] = status_id status_changed = True From 6d4a80cd30b8adf926a44b46c2c8f70ee04217f0 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 14:28:27 +0200 Subject: [PATCH 119/122] added some logs related to status changes --- .../plugins/publish/integrate_ftrack_farm_status.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py index c5fc3dd68f..ab5738c33f 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_farm_status.py @@ -113,10 +113,12 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): status_name_low = status_name.lower() status_id = None + status_name = None # Skip if status name was already tried to be found for status in task_statuses: if status["name"].lower() == status_name_low: status_id = status["id"] + status_name = status["name"] break if status_id is None: @@ -136,6 +138,13 @@ class IntegrateFtrackFarmStatus(pyblish.api.ContextPlugin): if status_id != task_entity["status_id"]: task_entity["status_id"] = status_id status_changed = True + path = "/".join([ + item["name"] + for item in task_entity["link"] + ]) + self.log.debug("Set status \"{}\" to \"{}\"".format( + status_name, path + )) if status_changed: session.commit() From 5a0b15c63b90a97417d43d9a3cfff7ba927dd4e1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Wed, 17 Aug 2022 15:35:46 +0200 Subject: [PATCH 120/122] fix typo in codec --- .../ftrack/plugins/publish/integrate_ftrack_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py index 3f0cc176a2..1bf4caac77 100644 --- a/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py +++ b/openpype/modules/ftrack/plugins/publish/integrate_ftrack_instances.py @@ -29,7 +29,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): "width": "Resolution width", "height": "Resolution height", "fps": "FPS", - "code": "Codec" + "codec": "Codec" } family_mapping = { @@ -526,7 +526,7 @@ class IntegrateFtrackInstance(pyblish.api.InstancePlugin): ("width", stream_width), ("height", stream_height), ("fps", fps), - ("code", codec_label) + ("codec", codec_label) ]: if not value or key not in self.additional_metadata_keys: continue From 09af23e2d789dcb02450cbd6eed9be53c062c416 Mon Sep 17 00:00:00 2001 From: Jakub Jezek Date: Wed, 17 Aug 2022 17:26:10 +0200 Subject: [PATCH 121/122] resolve: fixing import in collector --- .../hosts/resolve/plugins/publish/precollect_workfile.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py index 53e67aee0e..0f94216556 100644 --- a/openpype/hosts/resolve/plugins/publish/precollect_workfile.py +++ b/openpype/hosts/resolve/plugins/publish/precollect_workfile.py @@ -1,11 +1,9 @@ import pyblish.api from pprint import pformat -from importlib import reload -from openpype.hosts import resolve +from openpype.hosts.resolve import api as rapi from openpype.pipeline import legacy_io from openpype.hosts.resolve.otio import davinci_export -reload(davinci_export) class PrecollectWorkfile(pyblish.api.ContextPlugin): @@ -18,9 +16,9 @@ class PrecollectWorkfile(pyblish.api.ContextPlugin): asset = legacy_io.Session["AVALON_ASSET"] subset = "workfile" - project = resolve.get_current_project() + project = rapi.get_current_project() fps = project.GetSetting("timelineFrameRate") - video_tracks = resolve.get_video_track_names() + video_tracks = rapi.get_video_track_names() # adding otio timeline to context otio_timeline = davinci_export.create_otio_timeline(project) From da3268c9a75e04a8464589fc1c1153e264fec60a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Thu, 18 Aug 2022 09:51:45 +0200 Subject: [PATCH 122/122] resave default settings --- openpype/settings/defaults/project_settings/ftrack.json | 2 +- openpype/settings/defaults/project_settings/shotgrid.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 58b6a55958..2d5f889aa5 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -498,4 +498,4 @@ "farm_status_profiles": [] } } -} +} \ No newline at end of file diff --git a/openpype/settings/defaults/project_settings/shotgrid.json b/openpype/settings/defaults/project_settings/shotgrid.json index 83b6f69074..774bce714b 100644 --- a/openpype/settings/defaults/project_settings/shotgrid.json +++ b/openpype/settings/defaults/project_settings/shotgrid.json @@ -19,4 +19,4 @@ "step": "step" } } -} +} \ No newline at end of file