From 7f83c8a2d028ddeeb772d1bcc7e4a0348568ee25 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 15:21:42 +0100 Subject: [PATCH 001/291] OP-2765 - added methods for New Publisher Removed uuid, replaced with instance_id or first members item --- openpype/hosts/aftereffects/api/__init__.py | 8 ++++- openpype/hosts/aftereffects/api/pipeline.py | 39 +++++++++++++++------ openpype/hosts/aftereffects/api/ws_stub.py | 20 +++++------ 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/aftereffects/api/__init__.py b/openpype/hosts/aftereffects/api/__init__.py index cea1bdc023..2ad1255d27 100644 --- a/openpype/hosts/aftereffects/api/__init__.py +++ b/openpype/hosts/aftereffects/api/__init__.py @@ -16,7 +16,10 @@ from .pipeline import ( uninstall, list_instances, remove_instance, - containerise + containerise, + get_context_data, + update_context_data, + get_context_title ) from .workio import ( @@ -51,6 +54,9 @@ __all__ = [ "list_instances", "remove_instance", "containerise", + "get_context_data", + "update_context_data", + "get_context_title", "file_extensions", "has_unsaved_changes", diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 94f1e3d105..ea03542765 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -10,6 +10,7 @@ from avalon import io, pipeline from openpype import lib from openpype.api import Logger import openpype.hosts.aftereffects +from openpype.pipeline import BaseCreator from .launch_logic import get_stub @@ -67,6 +68,7 @@ def install(): avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(BaseCreator, CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -238,12 +240,6 @@ def list_instances(): if instance.get("schema") and \ "container" in instance.get("schema"): continue - - uuid_val = instance.get("uuid") - if uuid_val: - instance['uuid'] = uuid_val - else: - instance['uuid'] = instance.get("members")[0] # legacy instances.append(instance) return instances @@ -265,8 +261,29 @@ def remove_instance(instance): if not stub: return - stub.remove_instance(instance.get("uuid")) - item = stub.get_item(instance.get("uuid")) - if item: - stub.rename_item(item.id, - item.name.replace(stub.PUBLISH_ICON, '')) + inst_id = instance.get("instance_id") + if not inst_id: + log.warning("No instance identifier for {}".format(instance)) + return + + stub.remove_instance(inst_id) + + if instance.members: + item = stub.get_item(instance.members[0]) + if item: + stub.rename_item(item.id, + item.name.replace(stub.PUBLISH_ICON, '')) + + +def get_context_data(): + print("get_context_data") + return {} + + +def update_context_data(data, changes): + print("update_context_data") + + +def get_context_title(): + """Returns title for Creator window""" + return "AfterEffects" diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 5a0600e92e..d098419e81 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -28,6 +28,7 @@ class AEItem(object): workAreaDuration = attr.ib(default=None) frameRate = attr.ib(default=None) file_name = attr.ib(default=None) + instance_id = attr.ib(default=None) # New Publisher class AfterEffectsServerStub(): @@ -132,8 +133,9 @@ class AfterEffectsServerStub(): is_new = True for item_meta in items_meta: - if item_meta.get('members') \ - and str(item.id) == str(item_meta.get('members')[0]): + if ((item_meta.get('members') and + str(item.id) == str(item_meta.get('members')[0])) or + item_meta.get("instance_id") == item.id): is_new = False if data: item_meta.update(data) @@ -314,15 +316,12 @@ class AfterEffectsServerStub(): Keep matching item in file though. Args: - instance_id(string): instance uuid + instance_id(string): instance id """ cleaned_data = [] for instance in self.get_metadata(): - uuid_val = instance.get("uuid") - if not uuid_val: - uuid_val = instance.get("members")[0] # legacy - if uuid_val != instance_id: + if instance.get("instance_id") != instance_id: cleaned_data.append(instance) payload = json.dumps(cleaned_data, indent=4) @@ -357,7 +356,7 @@ class AfterEffectsServerStub(): item_id (int): Returns: - (namedtuple) + (AEItem) """ res = self.websocketserver.call(self.client.call @@ -418,7 +417,7 @@ class AfterEffectsServerStub(): """ Get render queue info for render purposes Returns: - (namedtuple): with 'file_name' field + (AEItem): with 'file_name' field """ res = self.websocketserver.call(self.client.call ('AfterEffects.get_render_info')) @@ -606,7 +605,8 @@ class AfterEffectsServerStub(): d.get('workAreaStart'), d.get('workAreaDuration'), d.get('frameRate'), - d.get('file_name')) + d.get('file_name'), + d.get("instance_id")) ret.append(item) return ret From 2af112571dd0435b639c78c4ccac9f185e1338e6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 15:26:56 +0100 Subject: [PATCH 002/291] OP-2765 - refactor - order of methods changed --- openpype/hosts/aftereffects/api/pipeline.py | 187 ++++++++++---------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index ea03542765..1ec76fd9dd 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -27,39 +27,6 @@ CREATE_PATH = os.path.join(PLUGINS_DIR, "create") INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") -def check_inventory(): - if not lib.any_outdated(): - return - - host = pyblish.api.registered_host() - outdated_containers = [] - for container in host.ls(): - representation = container['representation'] - representation_doc = io.find_one( - { - "_id": io.ObjectId(representation), - "type": "representation" - }, - projection={"parent": True} - ) - if representation_doc and not lib.is_latest(representation_doc): - outdated_containers.append(container) - - # Warn about outdated containers. - print("Starting new QApplication..") - app = QtWidgets.QApplication(sys.argv) - - message_box = QtWidgets.QMessageBox() - message_box.setIcon(QtWidgets.QMessageBox.Warning) - msg = "There are outdated containers in the scene." - message_box.setText(msg) - message_box.exec_() - - -def application_launch(): - check_inventory() - - def install(): print("Installing Pype config...") @@ -84,6 +51,11 @@ def uninstall(): avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) +def application_launch(): + """Triggered after start of app""" + check_inventory() + + def on_pyblish_instance_toggled(instance, old_value, new_value): """Toggle layer visibility on instance toggles.""" instance[0].Visible = new_value @@ -118,6 +90,77 @@ def get_asset_settings(): } +# loaded containers section +def ls(): + """Yields containers from active AfterEffects document. + + This is the host-equivalent of api.ls(), but instead of listing + assets on disk, it lists assets already loaded in AE; once loaded + they are called 'containers'. Used in Manage tool. + + Containers could be on multiple levels, single images/videos/was as a + FootageItem, or multiple items - backgrounds (folder with automatically + created composition and all imported layers). + + Yields: + dict: container + + """ + try: + stub = get_stub() # only after AfterEffects is up + except lib.ConnectionNotEstablishedYet: + print("Not connected yet, ignoring") + return + + layers_meta = stub.get_metadata() + for item in stub.get_items(comps=True, + folders=True, + footages=True): + data = stub.read(item, layers_meta) + # Skip non-tagged layers. + if not data: + continue + + # Filter to only containers. + if "container" not in data["id"]: + continue + + # Append transient data + data["objectName"] = item.name.replace(stub.LOADED_ICON, '') + data["layer"] = item + yield data + + +def check_inventory(): + """Checks loaded containers if they are of highest version""" + if not lib.any_outdated(): + return + + host = pyblish.api.registered_host() + outdated_containers = [] + for container in host.ls(): + representation = container['representation'] + representation_doc = io.find_one( + { + "_id": io.ObjectId(representation), + "type": "representation" + }, + projection={"parent": True} + ) + if representation_doc and not lib.is_latest(representation_doc): + outdated_containers.append(container) + + # Warn about outdated containers. + print("Starting new QApplication..") + app = QtWidgets.QApplication(sys.argv) + + message_box = QtWidgets.QMessageBox() + message_box.setIcon(QtWidgets.QMessageBox.Warning) + msg = "There are outdated containers in the scene." + message_box.setText(msg) + message_box.exec_() + + def containerise(name, namespace, comp, @@ -159,64 +202,7 @@ def containerise(name, return comp -def _get_stub(): - """ - Handle pulling stub from PS to run operations on host - Returns: - (AEServerStub) or None - """ - try: - stub = get_stub() # only after Photoshop is up - except lib.ConnectionNotEstablishedYet: - print("Not connected yet, ignoring") - return - - if not stub.get_active_document_name(): - return - - return stub - - -def ls(): - """Yields containers from active AfterEffects document. - - This is the host-equivalent of api.ls(), but instead of listing - assets on disk, it lists assets already loaded in AE; once loaded - they are called 'containers'. Used in Manage tool. - - Containers could be on multiple levels, single images/videos/was as a - FootageItem, or multiple items - backgrounds (folder with automatically - created composition and all imported layers). - - Yields: - dict: container - - """ - try: - stub = get_stub() # only after AfterEffects is up - except lib.ConnectionNotEstablishedYet: - print("Not connected yet, ignoring") - return - - layers_meta = stub.get_metadata() - for item in stub.get_items(comps=True, - folders=True, - footages=True): - data = stub.read(item, layers_meta) - # Skip non-tagged layers. - if not data: - continue - - # Filter to only containers. - if "container" not in data["id"]: - continue - - # Append transient data - data["objectName"] = item.name.replace(stub.LOADED_ICON, '') - data["layer"] = item - yield data - - +# created instances section def list_instances(): """ List all created instances from current workfile which @@ -275,6 +261,7 @@ def remove_instance(instance): item.name.replace(stub.PUBLISH_ICON, '')) +# new publisher section def get_context_data(): print("get_context_data") return {} @@ -287,3 +274,21 @@ def update_context_data(data, changes): def get_context_title(): """Returns title for Creator window""" return "AfterEffects" + + +def _get_stub(): + """ + Handle pulling stub from PS to run operations on host + Returns: + (AEServerStub) or None + """ + try: + stub = get_stub() # only after Photoshop is up + except lib.ConnectionNotEstablishedYet: + print("Not connected yet, ignoring") + return + + if not stub.get_active_document_name(): + return + + return stub From a27119bee40d29725eea5493e1b2004d1813669d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:26:04 +0100 Subject: [PATCH 003/291] OP-2765 - renamed old creators --- ...ender.py => create_legacy_local_render.py} | 6 +- .../plugins/create/create_legacy_render.py | 62 +++++++++++++++++++ 2 files changed, 65 insertions(+), 3 deletions(-) rename openpype/hosts/aftereffects/plugins/create/{create_local_render.py => create_legacy_local_render.py} (57%) create mode 100644 openpype/hosts/aftereffects/plugins/create/create_legacy_render.py diff --git a/openpype/hosts/aftereffects/plugins/create/create_local_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py similarity index 57% rename from openpype/hosts/aftereffects/plugins/create/create_local_render.py rename to openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py index 9d2cdcd7be..4fb07f31f8 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_local_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py @@ -1,7 +1,7 @@ -from openpype.hosts.aftereffects.plugins.create import create_render +from openpype.hosts.aftereffects.plugins.create import create_legacy_render -class CreateLocalRender(create_render.CreateRender): +class CreateLocalRender(create_legacy_render.CreateRender): """ Creator to render locally. Created only after default render on farm. So family 'render.local' is @@ -10,4 +10,4 @@ class CreateLocalRender(create_render.CreateRender): name = "renderDefault" label = "Render Locally" - family = "renderLocal" + family = "renderLocal" \ No newline at end of file diff --git a/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py new file mode 100644 index 0000000000..7da489a731 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py @@ -0,0 +1,62 @@ +from avalon.api import CreatorError + +import openpype.api +from openpype.hosts.aftereffects.api import ( + get_stub, + list_instances +) + + +class CreateRender(openpype.api.Creator): + """Render folder for publish. + + Creates subsets in format 'familyTaskSubsetname', + eg 'renderCompositingMain'. + + Create only single instance from composition at a time. + """ + + name = "renderDefault" + label = "Render on Farm" + family = "render" + defaults = ["Main"] + + def process(self): + stub = get_stub() # only after After Effects is up + if (self.options or {}).get("useSelection"): + items = stub.get_selected_items( + comps=True, folders=False, footages=False + ) + if len(items) > 1: + raise CreatorError( + "Please select only single composition at time." + ) + + if not items: + raise CreatorError(( + "Nothing to create. Select composition " + "if 'useSelection' or create at least " + "one composition." + )) + + existing_subsets = [ + instance['subset'].lower() + for instance in list_instances() + ] + + item = items.pop() + if self.name.lower() in existing_subsets: + txt = "Instance with name \"{}\" already exists.".format(self.name) + raise CreatorError(txt) + + self.data["members"] = [item.id] + self.data["uuid"] = item.id # for SubsetManager + self.data["subset"] = ( + self.data["subset"] + .replace(stub.PUBLISH_ICON, '') + .replace(stub.LOADED_ICON, '') + ) + + stub.imprint(item, self.data) + stub.set_label_color(item.id, 14) # Cyan options 0 - 16 + stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) \ No newline at end of file From ebc05e82c8001878667aa31d1cba014d9c06f231 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:31:18 +0100 Subject: [PATCH 004/291] OP-2765 - refactored imprint method Uses id instead of full AEItem --- openpype/hosts/aftereffects/api/pipeline.py | 8 ++++---- openpype/hosts/aftereffects/api/ws_stub.py | 8 ++++---- .../hosts/aftereffects/plugins/load/load_background.py | 5 ++--- openpype/hosts/aftereffects/plugins/load/load_file.py | 8 ++++---- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 1ec76fd9dd..550ff25886 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -178,7 +178,7 @@ def containerise(name, Arguments: name (str): Name of resulting assembly namespace (str): Namespace under which to host container - comp (Comp): Composition to containerise + comp (AEItem): Composition to containerise context (dict): Asset information loader (str, optional): Name of loader used to produce this container. suffix (str, optional): Suffix of container, defaults to `_CON`. @@ -197,7 +197,7 @@ def containerise(name, } stub = get_stub() - stub.imprint(comp, data) + stub.imprint(comp.id, data) return comp @@ -254,8 +254,8 @@ def remove_instance(instance): stub.remove_instance(inst_id) - if instance.members: - item = stub.get_item(instance.members[0]) + if instance.get("members"): + item = stub.get_item(instance["members"][0]) if item: stub.rename_item(item.id, item.name.replace(stub.PUBLISH_ICON, '')) diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index d098419e81..18852d3d6c 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -111,11 +111,11 @@ class AfterEffectsServerStub(): self.log.debug("Couldn't find layer metadata") - def imprint(self, item, data, all_items=None, items_meta=None): + def imprint(self, item_id, data, all_items=None, items_meta=None): """ Save item metadata to Label field of metadata of active document Args: - item (AEItem): + item_id (int|str): id of FootageItem or instance_id for workfiles data(string): json representation for single layer all_items (list of item): for performance, could be injected for usage in loop, if not, single call will be @@ -134,8 +134,8 @@ class AfterEffectsServerStub(): for item_meta in items_meta: if ((item_meta.get('members') and - str(item.id) == str(item_meta.get('members')[0])) or - item_meta.get("instance_id") == item.id): + str(item_id) == str(item_meta.get('members')[0])) or + item_meta.get("instance_id") == item_id): is_new = False if data: item_meta.update(data) diff --git a/openpype/hosts/aftereffects/plugins/load/load_background.py b/openpype/hosts/aftereffects/plugins/load/load_background.py index 1a2d6fc432..9b39556040 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_background.py +++ b/openpype/hosts/aftereffects/plugins/load/load_background.py @@ -91,7 +91,7 @@ class BackgroundLoader(AfterEffectsLoader): container["namespace"] = comp_name container["members"] = comp.members - stub.imprint(comp, container) + stub.imprint(comp.id, container) def remove(self, container): """ @@ -100,10 +100,9 @@ class BackgroundLoader(AfterEffectsLoader): Args: container (dict): container to be removed - used to get layer_id """ - print("!!!! container:: {}".format(container)) stub = self.get_stub() layer = container.pop("layer") - stub.imprint(layer, {}) + stub.imprint(layer.id, {}) stub.delete_item(layer.id) def switch(self, container, representation): diff --git a/openpype/hosts/aftereffects/plugins/load/load_file.py b/openpype/hosts/aftereffects/plugins/load/load_file.py index 9dbbf7aae1..ba5bb5f69a 100644 --- a/openpype/hosts/aftereffects/plugins/load/load_file.py +++ b/openpype/hosts/aftereffects/plugins/load/load_file.py @@ -96,9 +96,9 @@ class FileLoader(AfterEffectsLoader): # with aftereffects.maintained_selection(): # TODO stub.replace_item(layer.id, path, stub.LOADED_ICON + layer_name) stub.imprint( - layer, {"representation": str(representation["_id"]), - "name": context["subset"], - "namespace": layer_name} + layer.id, {"representation": str(representation["_id"]), + "name": context["subset"], + "namespace": layer_name} ) def remove(self, container): @@ -109,7 +109,7 @@ class FileLoader(AfterEffectsLoader): """ stub = self.get_stub() layer = container.pop("layer") - stub.imprint(layer, {}) + stub.imprint(layer.id, {}) stub.delete_item(layer.id) def switch(self, container, representation): From 3c11f46b110d3e74f96b7990845bec375ee46d05 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:32:04 +0100 Subject: [PATCH 005/291] OP-2765 - working version of new creator --- .../plugins/create/create_render.py | 126 ++++++++++++------ 1 file changed, 87 insertions(+), 39 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 8dfc85cdc8..c290bd46c3 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,37 +1,65 @@ -from avalon.api import CreatorError - -import openpype.api -from openpype.hosts.aftereffects.api import ( - get_stub, - list_instances +import json +from openpype import resources +import openpype.hosts.aftereffects.api as api +from openpype.pipeline import ( + Creator, + CreatedInstance, + lib, + CreatorError ) -class CreateRender(openpype.api.Creator): - """Render folder for publish. - - Creates subsets in format 'familyTaskSubsetname', - eg 'renderCompositingMain'. - - Create only single instance from composition at a time. - """ - - name = "renderDefault" - label = "Render on Farm" +class RenderCreator(Creator): + identifier = "render" + label = "Render" family = "render" - defaults = ["Main"] + description = "Render creator" - def process(self): - stub = get_stub() # only after After Effects is up - if (self.options or {}).get("useSelection"): + create_allow_context_change = False + + def get_icon(self): + return resources.get_openpype_splash_filepath() + + def collect_instances(self): + for instance_data in api.list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + instance_data = self._handle_legacy(instance_data) + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + created_inst, changes = update_list[0] + print("RenderCreator update_list:: {}-{}".format(created_inst, changes)) + api.get_stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) + + def remove_instances(self, instances): + for instance in instances: + print("instance:: {}".format(instance)) + api.remove_instance(instance) + self._remove_instance_from_context(instance) + + def create(self, subset_name, data, pre_create_data): + print("Data that can be used in create:\n{}".format( + json.dumps(pre_create_data, indent=4) + )) + stub = api.get_stub() # only after After Effects is up + print("pre_create_data:: {}".format(pre_create_data)) + if pre_create_data.get("use_selection"): items = stub.get_selected_items( comps=True, folders=False, footages=False ) + else: + items = stub.get_items(comps=True, folders=False, footages=False) + if len(items) > 1: raise CreatorError( "Please select only single composition at time." ) - + print("items:: {}".format(items)) if not items: raise CreatorError(( "Nothing to create. Select composition " @@ -39,24 +67,44 @@ class CreateRender(openpype.api.Creator): "one composition." )) - existing_subsets = [ - instance['subset'].lower() - for instance in list_instances() + data["members"] = [items[0].id] + new_instance = CreatedInstance(self.family, subset_name, data, self) + new_instance.creator_attributes["farm"] = pre_create_data["farm"] + + api.get_stub().imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + self.log.info(new_instance.data) + self._add_instance_to_context(new_instance) + + def get_default_variants(self): + return [ + "myVariant", + "variantTwo", + "different_variant" ] - item = items.pop() - if self.name.lower() in existing_subsets: - txt = "Instance with name \"{}\" already exists.".format(self.name) - raise CreatorError(txt) + def get_instance_attr_defs(self): + return [lib.BoolDef("farm", label="Render on farm")] - self.data["members"] = [item.id] - self.data["uuid"] = item.id # for SubsetManager - self.data["subset"] = ( - self.data["subset"] - .replace(stub.PUBLISH_ICON, '') - .replace(stub.LOADED_ICON, '') - ) + def get_pre_create_attr_defs(self): + output = [ + lib.BoolDef("use_selection", default=True, label="Use selection"), + lib.UISeparatorDef(), + lib.BoolDef("farm", label="Render on farm") + ] + return output + + def get_detail_description(self): + return """Creator for Render instances""" + + def _handle_legacy(self, instance_data): + """Converts old instances to new format.""" + if instance_data.get("uuid"): + instance_data["item_id"] = instance_data.get("uuid") + instance_data.pop("uuid") + + if not instance_data.get("members"): + instance_data["members"] = [instance_data["item_id"]] + + return instance_data - stub.imprint(item, self.data) - stub.set_label_color(item.id, 14) # Cyan options 0 - 16 - stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) From 082b2306ee08a4f286804d1afe0f8139006e5fe8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:32:50 +0100 Subject: [PATCH 006/291] OP-2765 - changed collector to work with new creator --- .../hosts/aftereffects/plugins/publish/collect_workfile.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index c1c2be4855..61c4897cae 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -10,6 +10,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.1 def process(self, context): + for instance in context: + if instance.data["family"] == "workfile": + self.log.debug("Workfile instance found, skipping") + return + task = api.Session["AVALON_TASK"] current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) From 64b63369d6b1a8bbf702a3fe34a3ea05e4021d79 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:33:21 +0100 Subject: [PATCH 007/291] OP-2765 - added 'newPublishing' flag to differentiate --- openpype/plugins/publish/collect_from_create_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 16e3f669c3..09584ab37c 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -25,7 +25,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): # Update global data to context context.data.update(create_context.context_data_to_store()) - + context.data["newPublishing"] = True # Update context data for key in ("AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK"): value = create_context.dbcon.Session.get(key) From be05fe990580aff0bc98ffee8243bc4e7536083e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:34:00 +0100 Subject: [PATCH 008/291] OP-2765 - updated collecting of render family Added pre collect for backward compatibility --- .../plugins/publish/collect_render.py | 197 ++++++++++-------- .../plugins/publish/pre_collect_render.py | 47 +++++ 2 files changed, 154 insertions(+), 90 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 2a4b773681..1ad3d3dd18 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -2,6 +2,7 @@ import os import re import tempfile import attr +from copy import deepcopy import pyblish.api @@ -29,20 +30,22 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): label = "Collect After Effects Render Layers" hosts = ["aftereffects"] - # internal - family_remapping = { - "render": ("render.farm", "farm"), # (family, label) - "renderLocal": ("render", "local") - } padding_width = 6 rendered_extension = 'png' - stub = get_stub() + _stub = None + + @classmethod + def get_stub(cls): + if not cls._stub: + cls._stub = get_stub() + return cls._stub def get_instances(self, context): instances = [] + instances_to_remove = [] - app_version = self.stub.get_app_version() + app_version = CollectAERender.get_stub().get_app_version() app_version = app_version[0:4] current_file = context.data["currentFile"] @@ -50,105 +53,91 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - compositions = self.stub.get_items(True) + compositions = CollectAERender.get_stub().get_items(True) compositions_by_id = {item.id: item for item in compositions} - for inst in self.stub.get_metadata(): - schema = inst.get('schema') - # loaded asset container skip it - if schema and 'container' in schema: + for inst in context: + family = inst.data["family"] + if family != "render": continue + self._debug_log(inst) - if not inst["members"]: - raise ValueError("Couldn't find id, unable to publish. " + - "Please recreate instance.") - item_id = inst["members"][0] + item_id = inst.data["members"][0] - work_area_info = self.stub.get_work_area(int(item_id)) + work_area_info = CollectAERender.get_stub().get_work_area( + int(item_id)) if not work_area_info: self.log.warning("Orphaned instance, deleting metadata") - self.stub.remove_instance(int(item_id)) + inst_id = inst.get("instance_id") or item_id + CollectAERender.get_stub().remove_instance(inst_id) continue - frameStart = work_area_info.workAreaStart - - frameEnd = round(work_area_info.workAreaStart + - float(work_area_info.workAreaDuration) * - float(work_area_info.frameRate)) - 1 + frame_start = work_area_info.workAreaStart + frame_end = round(work_area_info.workAreaStart + + float(work_area_info.workAreaDuration) * + float(work_area_info.frameRate)) - 1 fps = work_area_info.frameRate # TODO add resolution when supported by extension - if inst["family"] in self.family_remapping.keys() \ - and inst["active"]: - remapped_family = self.family_remapping[inst["family"]] - instance = AERenderInstance( - family=remapped_family[0], - families=[remapped_family[0]], - version=version, - time="", - source=current_file, - label="{} - {}".format(inst["subset"], remapped_family[1]), - subset=inst["subset"], - asset=context.data["assetEntity"]["name"], - attachTo=False, - setMembers='', - publish=True, - renderer='aerender', - name=inst["subset"], - resolutionWidth=asset_entity["data"].get( - "resolutionWidth", - project_entity["data"]["resolutionWidth"]), - resolutionHeight=asset_entity["data"].get( - "resolutionHeight", - project_entity["data"]["resolutionHeight"]), - pixelAspect=1, - tileRendering=False, - tilesX=0, - tilesY=0, - frameStart=frameStart, - frameEnd=frameEnd, - frameStep=1, - toBeRenderedOn='deadline', - fps=fps, - app_version=app_version - ) + if not inst.data["active"]: + continue - comp = compositions_by_id.get(int(item_id)) - if not comp: - raise ValueError("There is no composition for item {}". - format(item_id)) - instance.comp_name = comp.name - instance.comp_id = item_id - instance._anatomy = context.data["anatomy"] - instance.anatomyData = context.data["anatomyData"] + subset_name = inst.data["subset"] + instance = AERenderInstance( + family=family, + families=[family], + version=version, + time="", + source=current_file, + label="{} - {}".format(subset_name, family), + subset=subset_name, + asset=context.data["assetEntity"]["name"], + attachTo=False, + setMembers='', + publish=True, + renderer='aerender', + name=subset_name, + resolutionWidth=asset_entity["data"].get( + "resolutionWidth", + project_entity["data"]["resolutionWidth"]), + resolutionHeight=asset_entity["data"].get( + "resolutionHeight", + project_entity["data"]["resolutionHeight"]), + pixelAspect=1, + tileRendering=False, + tilesX=0, + tilesY=0, + frameStart=frame_start, + frameEnd=frame_end, + frameStep=1, + toBeRenderedOn='deadline', + fps=fps, + app_version=app_version, + anatomyData=deepcopy(context.data["anatomyData"]), + context=context + ) - instance.outputDir = self._get_output_dir(instance) - instance.context = context + comp = compositions_by_id.get(int(item_id)) + if not comp: + raise ValueError("There is no composition for item {}". + format(item_id)) + instance.outputDir = self._get_output_dir(instance) + instance.comp_name = comp.name + instance.comp_id = item_id - settings = get_project_settings(os.getenv("AVALON_PROJECT")) - reviewable_subset_filter = \ - (settings["deadline"] - ["publish"] - ["ProcessSubmittedJobOnFarm"] - ["aov_filter"]) + is_local = "renderLocal" in inst.data["families"] + if inst.data.get("creator_attributes"): + is_local = inst.data["creator_attributes"].get("farm") + if is_local: + # for local renders + instance = self._update_for_local(instance, project_entity) - if inst["family"] == "renderLocal": - # for local renders - instance.anatomyData["version"] = instance.version - instance.anatomyData["subset"] = instance.subset - instance.stagingDir = tempfile.mkdtemp() - instance.projectEntity = project_entity + self.log.info("New instance:: {}".format(instance)) + instances.append(instance) + instances_to_remove.append(inst) - if self.hosts[0] in reviewable_subset_filter.keys(): - for aov_pattern in \ - reviewable_subset_filter[self.hosts[0]]: - if re.match(aov_pattern, instance.subset): - instance.families.append("review") - instance.review = True - break - - self.log.info("New instance:: {}".format(instance)) - instances.append(instance) + for instance in instances_to_remove: + context.remove(instance) return instances @@ -169,7 +158,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): end = render_instance.frameEnd # pull file name from Render Queue Output module - render_q = self.stub.get_render_info() + render_q = CollectAERender.get_stub().get_render_info() if not render_q: raise ValueError("No file extension set in Render Queue") _, ext = os.path.splitext(os.path.basename(render_q.file_name)) @@ -216,3 +205,31 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): # for submit_publish_job return base_dir + + def _update_for_local(self, instance, project_entity): + instance.anatomyData["version"] = instance.version + instance.anatomyData["subset"] = instance.subset + instance.stagingDir = tempfile.mkdtemp() + instance.projectEntity = project_entity + + settings = get_project_settings(os.getenv("AVALON_PROJECT")) + reviewable_subset_filter = (settings["deadline"] + ["publish"] + ["ProcessSubmittedJobOnFarm"] + ["aov_filter"].get(self.hosts[0])) + for aov_pattern in reviewable_subset_filter: + if re.match(aov_pattern, instance.subset): + instance.families.append("review") + instance.review = True + break + + return instance + + def _debug_log(self, instance): + def _default_json(value): + return str(value) + + import json + self.log.info( + json.dumps(instance.data, indent=4, default=_default_json) + ) diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py new file mode 100644 index 0000000000..56dc884634 --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -0,0 +1,47 @@ +import json +import pyblish.api +from openpype.hosts.aftereffects.api import get_stub, list_instances + + +class PreCollectRender(pyblish.api.ContextPlugin): + """ + Checks if render instance is of new type, adds to families to both + existing collectors work same way. + """ + + label = "PreCollect Render" + order = pyblish.api.CollectorOrder + 0.400 + hosts = ["aftereffects"] + + family_remapping = { + "render": ("render.farm", "farm"), # (family, label) + "renderLocal": ("render", "local") + } + + def process(self, context): + if context.data.get("newPublishing"): + self.log.debug("Not applicable for New Publisher, skip") + return + + stub = get_stub() + for inst in list_instances(): + if inst["family"] not in self.family_remapping.keys(): + continue + + if not inst["members"]: + raise ValueError("Couldn't find id, unable to publish. " + + "Please recreate instance.") + + instance = context.create_instance(inst["subset"]) + inst["families"] = [self.family_remapping[inst["family"]]] + instance.data.update(inst) + + self._debug_log(instance) + + def _debug_log(self, instance): + def _default_json(value): + return str(value) + + self.log.info( + json.dumps(instance.data, indent=4, default=_default_json) + ) From c189725f3fdd7babae5709b70fd61708ae67bd91 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:34:27 +0100 Subject: [PATCH 009/291] OP-2765 - missed update for imprint --- .../aftereffects/plugins/publish/validate_instance_asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py index 71c1750457..3019719947 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_instance_asset.py @@ -27,7 +27,7 @@ class ValidateInstanceAssetRepair(pyblish.api.Action): data = stub.read(instance[0]) data["asset"] = api.Session["AVALON_ASSET"] - stub.imprint(instance[0], data) + stub.imprint(instance[0].instance_id, data) class ValidateInstanceAsset(pyblish.api.InstancePlugin): From 7967496b5c64c3e1a5c126de7c0a3f90dd3e81f5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 3 Mar 2022 19:34:52 +0100 Subject: [PATCH 010/291] OP-2765 - added CreatorError to pipeline api --- openpype/pipeline/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index e968df4011..2b7a39d444 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -4,7 +4,8 @@ from .create import ( BaseCreator, Creator, AutoCreator, - CreatedInstance + CreatedInstance, + CreatorError ) from .publish import ( @@ -21,6 +22,7 @@ __all__ = ( "Creator", "AutoCreator", "CreatedInstance", + "CreatorError", "PublishValidationError", "KnownPublishError", From 4434a4b1888f65a55aa86a365d186aabb6ec69cf Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Mar 2022 15:44:19 +0100 Subject: [PATCH 011/291] OP-2765 - added default to Setting for subset name of workfile in AE --- openpype/settings/defaults/project_settings/global.json | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/settings/defaults/project_settings/global.json b/openpype/settings/defaults/project_settings/global.json index f08bee8b2d..71c837659e 100644 --- a/openpype/settings/defaults/project_settings/global.json +++ b/openpype/settings/defaults/project_settings/global.json @@ -268,6 +268,7 @@ "workfile" ], "hosts": [ + "aftereffects", "tvpaint" ], "task_types": [], From e24ef3a9eba62a9dbcae252dcf70d9608145724b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Mar 2022 16:32:16 +0100 Subject: [PATCH 012/291] OP-2765 - added workfile creator and modified collector Workfile collector shouldn't create new isntance for NP, but should update version --- .../plugins/create/workfile_creator.py | 75 +++++++++++++++++++ .../plugins/publish/collect_workfile.py | 33 ++++---- 2 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 openpype/hosts/aftereffects/plugins/create/workfile_creator.py diff --git a/openpype/hosts/aftereffects/plugins/create/workfile_creator.py b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py new file mode 100644 index 0000000000..2d9d42ee8c --- /dev/null +++ b/openpype/hosts/aftereffects/plugins/create/workfile_creator.py @@ -0,0 +1,75 @@ +from avalon import io + +import openpype.hosts.aftereffects.api as api +from openpype.pipeline import ( + AutoCreator, + CreatedInstance +) + + +class AEWorkfileCreator(AutoCreator): + identifier = "workfile" + family = "workfile" + + def get_instance_attr_defs(self): + return [] + + def collect_instances(self): + for instance_data in api.list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + subset_name = instance_data["subset"] + instance = CreatedInstance( + self.family, subset_name, instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + # nothing to change on workfiles + pass + + def create(self, options=None): + existing_instance = None + for instance in self.create_context.instances: + if instance.family == self.family: + existing_instance = instance + break + + variant = '' + project_name = io.Session["AVALON_PROJECT"] + asset_name = io.Session["AVALON_ASSET"] + task_name = io.Session["AVALON_TASK"] + host_name = io.Session["AVALON_APP"] + + if existing_instance is None: + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant + } + data.update(self.get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name + )) + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) + + api.get_stub().imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 61c4897cae..29ec3a64e6 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -10,10 +10,11 @@ class CollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.1 def process(self, context): + create_instance = True for instance in context: if instance.data["family"] == "workfile": - self.log.debug("Workfile instance found, skipping") - return + self.log.debug("Workfile instance found, do not create new") + create_instance = False task = api.Session["AVALON_TASK"] current_file = context.data["currentFile"] @@ -44,20 +45,24 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # workfile instance family = "workfile" subset = family + task.capitalize() - # Create instance - instance = context.create_instance(subset) - # creating instance data - instance.data.update({ - "subset": subset, - "label": scene_file, - "family": family, - "families": [family], - "representations": list() - }) + if create_instance: # old publish + # Create instance + instance = context.create_instance(subset) - # adding basic script data - instance.data.update(shared_instance_data) + # creating instance data + instance.data.update({ + "subset": subset, + "label": scene_file, + "family": family, + "families": [family], + "representations": list() + }) + + # adding basic script data + instance.data.update(shared_instance_data) + else: + instance.data.update({"version": version}) # creating representation representation = { From 97b9b035db68132f22e4d48874a02ad5bf76c9af Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Mar 2022 17:54:21 +0100 Subject: [PATCH 013/291] OP-2765 - added helper logging function --- .../aftereffects/plugins/publish/collect_render.py | 13 +------------ .../plugins/publish/collect_workfile.py | 9 +++------ openpype/lib/__init__.py | 3 ++- openpype/lib/log.py | 12 ++++++++++++ 4 files changed, 18 insertions(+), 19 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 1ad3d3dd18..b41fb5d5f5 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -59,7 +59,6 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): family = inst.data["family"] if family != "render": continue - self._debug_log(inst) item_id = inst.data["members"][0] @@ -127,12 +126,11 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): is_local = "renderLocal" in inst.data["families"] if inst.data.get("creator_attributes"): - is_local = inst.data["creator_attributes"].get("farm") + is_local = not inst.data["creator_attributes"].get("farm") if is_local: # for local renders instance = self._update_for_local(instance, project_entity) - self.log.info("New instance:: {}".format(instance)) instances.append(instance) instances_to_remove.append(inst) @@ -224,12 +222,3 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): break return instance - - def _debug_log(self, instance): - def _default_json(value): - return str(value) - - import json - self.log.info( - json.dumps(instance.data, indent=4, default=_default_json) - ) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 29ec3a64e6..d8a324f828 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -2,6 +2,8 @@ import os from avalon import api import pyblish.api +from openpype.lib import debug_log_instance + class CollectWorkfile(pyblish.api.ContextPlugin): """ Adds the AE render instances """ @@ -61,8 +63,6 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # adding basic script data instance.data.update(shared_instance_data) - else: - instance.data.update({"version": version}) # creating representation representation = { @@ -74,7 +74,4 @@ class CollectWorkfile(pyblish.api.ContextPlugin): instance.data["representations"].append(representation) - self.log.info('Publishing After Effects workfile') - - for i in context: - self.log.debug(f"{i.data['families']}") + debug_log_instance(self.log, "Workfile instance", instance) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index 6a24f30455..fb7afe7cb3 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -35,7 +35,7 @@ from .execute import ( path_to_subprocess_arg, CREATE_NO_WINDOW ) -from .log import PypeLogger, timeit +from .log import PypeLogger, timeit, debug_log_instance from .path_templates import ( merge_dict, @@ -313,6 +313,7 @@ __all__ = [ "OpenPypeMongoConnection", "timeit", + "debug_log_instance", "is_overlapping_otio_ranges", "otio_range_with_handles", diff --git a/openpype/lib/log.py b/openpype/lib/log.py index a42faef008..7824e96159 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -23,6 +23,7 @@ import time import traceback import threading import copy +import json from . import Terminal from .mongo import ( @@ -493,3 +494,14 @@ def timeit(method): print('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) return result return timed + + +def debug_log_instance(logger, msg, instance): + """Helper function to write instance.data as json""" + def _default_json(value): + return str(value) + + logger.debug(msg) + logger.debug( + json.dumps(instance.data, indent=4, default=_default_json) + ) From 9065530eefdc98daf604d282f9f49e16614bcd0d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Mar 2022 18:20:36 +0100 Subject: [PATCH 014/291] OP-2765 - fixed wrong assignment of representations to instances --- .../aftereffects/plugins/publish/collect_workfile.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index d8a324f828..1bb476d80b 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -12,11 +12,12 @@ class CollectWorkfile(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder + 0.1 def process(self, context): - create_instance = True + existing_instance = None for instance in context: if instance.data["family"] == "workfile": - self.log.debug("Workfile instance found, do not create new") - create_instance = False + self.log.debug("Workfile instance found, won't create new") + existing_instance = instance + break task = api.Session["AVALON_TASK"] current_file = context.data["currentFile"] @@ -47,8 +48,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # workfile instance family = "workfile" subset = family + task.capitalize() - - if create_instance: # old publish + if existing_instance is None: # old publish # Create instance instance = context.create_instance(subset) @@ -63,6 +63,8 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # adding basic script data instance.data.update(shared_instance_data) + else: + instance = existing_instance # creating representation representation = { From 7b9ec117e7a32dd34d634d3a6d9ecaca54bb983f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 4 Mar 2022 19:02:05 +0100 Subject: [PATCH 015/291] OP-2765 - add fallback to uuid for backward compatibility --- openpype/hosts/aftereffects/api/pipeline.py | 2 +- openpype/hosts/aftereffects/api/ws_stub.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 550ff25886..4ae88e649a 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -247,7 +247,7 @@ def remove_instance(instance): if not stub: return - inst_id = instance.get("instance_id") + inst_id = instance.get("instance_id") or instance.get("uuid") # legacy if not inst_id: log.warning("No instance identifier for {}".format(instance)) return diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 18852d3d6c..1d3b69e038 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -321,7 +321,8 @@ class AfterEffectsServerStub(): cleaned_data = [] for instance in self.get_metadata(): - if instance.get("instance_id") != instance_id: + inst_id = instance.get("instance_id") or instance.get("uuid") + if inst_id != instance_id: cleaned_data.append(instance) payload = json.dumps(cleaned_data, indent=4) From 0e050d37e91d7730985cfae6d1eed62e97dd915b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 13:30:24 +0100 Subject: [PATCH 016/291] OP-2765 - fix legacy handling when creating --- .../plugins/create/create_render.py | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index c290bd46c3..0a907a02d8 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,4 +1,5 @@ -import json +import avalon.api + from openpype import resources import openpype.hosts.aftereffects.api as api from openpype.pipeline import ( @@ -22,7 +23,9 @@ class RenderCreator(Creator): def collect_instances(self): for instance_data in api.list_instances(): - creator_id = instance_data.get("creator_identifier") + # legacy instances have family=='render' or 'renderLocal', use them + creator_id = (instance_data.get("creator_identifier") or + instance_data.get("family").replace("Local", '')) if creator_id == self.identifier: instance_data = self._handle_legacy(instance_data) instance = CreatedInstance.from_existing( @@ -32,22 +35,16 @@ class RenderCreator(Creator): def update_instances(self, update_list): created_inst, changes = update_list[0] - print("RenderCreator update_list:: {}-{}".format(created_inst, changes)) api.get_stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) def remove_instances(self, instances): for instance in instances: - print("instance:: {}".format(instance)) api.remove_instance(instance) self._remove_instance_from_context(instance) def create(self, subset_name, data, pre_create_data): - print("Data that can be used in create:\n{}".format( - json.dumps(pre_create_data, indent=4) - )) stub = api.get_stub() # only after After Effects is up - print("pre_create_data:: {}".format(pre_create_data)) if pre_create_data.get("use_selection"): items = stub.get_selected_items( comps=True, folders=False, footages=False @@ -59,7 +56,6 @@ class RenderCreator(Creator): raise CreatorError( "Please select only single composition at time." ) - print("items:: {}".format(items)) if not items: raise CreatorError(( "Nothing to create. Select composition " @@ -73,7 +69,6 @@ class RenderCreator(Creator): api.get_stub().imprint(new_instance.get("instance_id"), new_instance.data_to_store()) - self.log.info(new_instance.data) self._add_instance_to_context(new_instance) def get_default_variants(self): @@ -99,12 +94,20 @@ class RenderCreator(Creator): def _handle_legacy(self, instance_data): """Converts old instances to new format.""" + if not instance_data.get("members"): + instance_data["members"] = [instance_data.get("uuid")] + if instance_data.get("uuid"): - instance_data["item_id"] = instance_data.get("uuid") + # uuid not needed, replaced with unique instance_id + api.get_stub().remove_instance(instance_data.get("uuid")) instance_data.pop("uuid") - if not instance_data.get("members"): - instance_data["members"] = [instance_data["item_id"]] + if not instance_data.get("task"): + instance_data["task"] = avalon.api.Session.get("AVALON_TASK") + + if not instance_data.get("creator_attributes"): + is_old_farm = instance_data["family"] != "renderLocal" + instance_data["creator_attributes"] = {"farm": is_old_farm} + instance_data["family"] = self.family return instance_data - From ca0a38f8de82e488e9353d1f1117a4e60620e41f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 13:32:26 +0100 Subject: [PATCH 017/291] OP-2765 - fixed exclude filter to user family or families properly Added render.farm to excluded, as in NP family is always 'render' --- openpype/plugins/publish/integrate_new.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/openpype/plugins/publish/integrate_new.py b/openpype/plugins/publish/integrate_new.py index 6e0940d459..581902205f 100644 --- a/openpype/plugins/publish/integrate_new.py +++ b/openpype/plugins/publish/integrate_new.py @@ -103,7 +103,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "hda", "usd" ] - exclude_families = ["clip"] + exclude_families = ["clip", "render.farm"] db_representation_context_keys = [ "project", "asset", "task", "subset", "version", "representation", "family", "hierarchy", "task", "username" @@ -121,11 +121,15 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): subset_grouping_profiles = None def process(self, instance): - self.integrated_file_sizes = {} - if [ef for ef in self.exclude_families - if instance.data["family"] in ef]: - return + for ef in self.exclude_families: + if ( + instance.data["family"] == ef or + ef in instance.data["families"]): + self.log.debug("Excluded family '{}' in '{}' or {}".format( + ef, instance.data["family"], instance.data["families"])) + return + self.integrated_file_sizes = {} try: self.register(instance) self.log.info("Integrated Asset in to the database ...") @@ -214,7 +218,10 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): # Ensure at least one file is set up for transfer in staging dir. repres = instance.data.get("representations") - assert repres, "Instance has no files to transfer" + repres = instance.data.get("representations") + msg = "Instance {} has no files to transfer".format( + instance.data["family"]) + assert repres, msg assert isinstance(repres, (list, tuple)), ( "Instance 'files' must be a list, got: {0} {1}".format( str(type(repres)), str(repres) From 296a2d162704b9ca0c1974d4b8093fe698760d6b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 13:34:12 +0100 Subject: [PATCH 018/291] OP-2765 - added publish flag to new instance of workfile --- openpype/hosts/aftereffects/plugins/publish/collect_workfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 1bb476d80b..67f037e6e6 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -65,6 +65,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): instance.data.update(shared_instance_data) else: instance = existing_instance + instance.data["publish"] = True # for DL # creating representation representation = { From 2d9bac166a466f8489e38997ec440c6f23476f26 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 13:35:02 +0100 Subject: [PATCH 019/291] OP-2765 - modified proper families renderLocal is legacy, should be removed in the future --- .../hosts/aftereffects/plugins/publish/extract_local_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py index b738068a7b..7323a0b125 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_local_render.py @@ -12,7 +12,7 @@ class ExtractLocalRender(openpype.api.Extractor): order = openpype.api.Extractor.order - 0.47 label = "Extract Local Render" hosts = ["aftereffects"] - families = ["render"] + families = ["renderLocal", "render.local"] def process(self, instance): stub = get_stub() From bf51f8452b8e2410d049f63389e3179bec31b600 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 13:40:41 +0100 Subject: [PATCH 020/291] OP-2765 - modified collect render plugin Should handle both legacy and new style of publishing --- .../hosts/aftereffects/plugins/publish/collect_render.py | 8 +++++--- .../aftereffects/plugins/publish/pre_collect_render.py | 9 +++++---- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index b41fb5d5f5..d31571b6b5 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -84,7 +84,6 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): subset_name = inst.data["subset"] instance = AERenderInstance( family=family, - families=[family], version=version, time="", source=current_file, @@ -124,19 +123,20 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): instance.comp_name = comp.name instance.comp_id = item_id - is_local = "renderLocal" in inst.data["families"] + is_local = "renderLocal" in inst.data["families"] # legacy if inst.data.get("creator_attributes"): is_local = not inst.data["creator_attributes"].get("farm") if is_local: # for local renders instance = self._update_for_local(instance, project_entity) + else: + instance.families = ["render.farm"] instances.append(instance) instances_to_remove.append(inst) for instance in instances_to_remove: context.remove(instance) - return instances def get_expected_files(self, render_instance): @@ -205,10 +205,12 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): return base_dir def _update_for_local(self, instance, project_entity): + """Update old saved instances to current publishing format""" instance.anatomyData["version"] = instance.version instance.anatomyData["subset"] = instance.subset instance.stagingDir = tempfile.mkdtemp() instance.projectEntity = project_entity + instance.families = ["render.local"] settings = get_project_settings(os.getenv("AVALON_PROJECT")) reviewable_subset_filter = (settings["deadline"] diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py index 56dc884634..614a04b4b7 100644 --- a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -1,12 +1,14 @@ import json import pyblish.api -from openpype.hosts.aftereffects.api import get_stub, list_instances +from openpype.hosts.aftereffects.api import list_instances class PreCollectRender(pyblish.api.ContextPlugin): """ - Checks if render instance is of new type, adds to families to both + Checks if render instance is of old type, adds to families to both existing collectors work same way. + + Could be removed in the future when no one uses old publish. """ label = "PreCollect Render" @@ -15,7 +17,7 @@ class PreCollectRender(pyblish.api.ContextPlugin): family_remapping = { "render": ("render.farm", "farm"), # (family, label) - "renderLocal": ("render", "local") + "renderLocal": ("render.local", "local") } def process(self, context): @@ -23,7 +25,6 @@ class PreCollectRender(pyblish.api.ContextPlugin): self.log.debug("Not applicable for New Publisher, skip") return - stub = get_stub() for inst in list_instances(): if inst["family"] not in self.family_remapping.keys(): continue From 9e3ea9139a06ad3cc495f8d0c43eb64a7eff8260 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 13:58:20 +0100 Subject: [PATCH 021/291] OP-2765 - Hound --- openpype/hosts/aftereffects/api/pipeline.py | 2 +- .../aftereffects/plugins/create/create_legacy_local_render.py | 2 +- .../hosts/aftereffects/plugins/create/create_legacy_render.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 4ae88e649a..4ade90e4dd 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -152,7 +152,7 @@ def check_inventory(): # Warn about outdated containers. print("Starting new QApplication..") - app = QtWidgets.QApplication(sys.argv) + _app = QtWidgets.QApplication(sys.argv) message_box = QtWidgets.QMessageBox() message_box.setIcon(QtWidgets.QMessageBox.Warning) diff --git a/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py index 4fb07f31f8..04413acbcf 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_local_render.py @@ -10,4 +10,4 @@ class CreateLocalRender(create_legacy_render.CreateRender): name = "renderDefault" label = "Render Locally" - family = "renderLocal" \ No newline at end of file + family = "renderLocal" diff --git a/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py index 7da489a731..8dfc85cdc8 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py @@ -59,4 +59,4 @@ class CreateRender(openpype.api.Creator): stub.imprint(item, self.data) stub.set_label_color(item.id, 14) # Cyan options 0 - 16 - stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) \ No newline at end of file + stub.rename_item(item.id, stub.PUBLISH_ICON + self.data["subset"]) From 3b72117a946d15954112b77107d04f325d30c0a3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 8 Mar 2022 19:11:55 +0100 Subject: [PATCH 022/291] OP-2765 - refactored validator --- .../publish/validate_scene_settings.py | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py index 273ccd295e..0753e3c09a 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -62,12 +62,13 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): expected_settings = get_asset_settings() self.log.info("config from DB::{}".format(expected_settings)) - if any(re.search(pattern, os.getenv('AVALON_TASK')) + task_name = instance.data["anatomyData"]["task"]["name"] + if any(re.search(pattern, task_name) for pattern in self.skip_resolution_check): expected_settings.pop("resolutionWidth") expected_settings.pop("resolutionHeight") - if any(re.search(pattern, os.getenv('AVALON_TASK')) + if any(re.search(pattern, task_name) for pattern in self.skip_timelines_check): expected_settings.pop('fps', None) expected_settings.pop('frameStart', None) @@ -87,10 +88,14 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): duration = instance.data.get("frameEndHandle") - \ instance.data.get("frameStartHandle") + 1 - self.log.debug("filtered config::{}".format(expected_settings)) + self.log.debug("validated items::{}".format(expected_settings)) current_settings = { "fps": fps, + "frameStart": instance.data.get("frameStart"), + "frameEnd": instance.data.get("frameEnd"), + "handleStart": instance.data.get("handleStart"), + "handleEnd": instance.data.get("handleEnd"), "frameStartHandle": instance.data.get("frameStartHandle"), "frameEndHandle": instance.data.get("frameEndHandle"), "resolutionWidth": instance.data.get("resolutionWidth"), @@ -103,24 +108,22 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): invalid_keys = set() for key, value in expected_settings.items(): if value != current_settings[key]: - invalid_settings.append( - "{} expected: {} found: {}".format(key, value, - current_settings[key]) - ) + msg = "'{}' expected: '{}' found: '{}'".format( + key, value, current_settings[key]) + + if key == "duration" and expected_settings.get("handleStart"): + msg += "Handles included in calculation. Remove " \ + "handles in DB or extend frame range in " \ + "Composition Setting." + + invalid_settings.append(msg) invalid_keys.add(key) - if ((expected_settings.get("handleStart") - or expected_settings.get("handleEnd")) - and invalid_settings): - msg = "Handles included in calculation. Remove handles in DB " +\ - "or extend frame range in Composition Setting." - invalid_settings[-1]["reason"] = msg - - msg = "Found invalid settings:\n{}".format( - "\n".join(invalid_settings) - ) - if invalid_settings: + msg = "Found invalid settings:\n{}".format( + "\n".join(invalid_settings) + ) + invalid_keys_str = ",".join(invalid_keys) break_str = "
" invalid_setting_str = "Found invalid settings:
{}".\ From 84b6a6cc6949ea849376f410417c9198a92a9241 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 10:20:58 +0100 Subject: [PATCH 023/291] OP-2868 - added configuration for default variant value to Settings --- .../plugins/create/create_render.py | 16 +++++++++---- .../project_settings/aftereffects.json | 7 ++++++ .../schema_project_aftereffects.json | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 0a907a02d8..e690af63d0 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -18,6 +18,16 @@ class RenderCreator(Creator): create_allow_context_change = False + def __init__( + self, create_context, system_settings, project_settings, headless=False + ): + super(RenderCreator, self).__init__(create_context, system_settings, + project_settings, headless) + self._default_variants = (project_settings["aftereffects"] + ["create"] + ["RenderCreator"] + ["defaults"]) + def get_icon(self): return resources.get_openpype_splash_filepath() @@ -72,11 +82,7 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) def get_default_variants(self): - return [ - "myVariant", - "variantTwo", - "different_variant" - ] + return self._default_variants def get_instance_attr_defs(self): return [lib.BoolDef("farm", label="Render on farm")] diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 6a9a399069..8083aa0972 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,4 +1,11 @@ { + "create": { + "RenderCreator": { + "defaults": [ + "Main" + ] + } + }, "publish": { "ValidateSceneSettings": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 4c4cd225ab..1a3eaef540 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -5,6 +5,29 @@ "label": "AfterEffects", "is_file": true, "children": [ + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "RenderCreator", + "label": "Create render", + "children": [ + { + "type": "list", + "key": "defaults", + "label": "Default Variants", + "object_type": "text", + "docstring": "Fill default variant(s) (like 'Main' or 'Default') used in subset name creation." + } + ] + } + ] + }, { "type": "dict", "collapsible": true, From 87d114a272cac020f1a482b6209ad01a9907ba01 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 10:49:43 +0100 Subject: [PATCH 024/291] OP-2765 - added error message when creating same subset --- openpype/hosts/aftereffects/plugins/create/create_render.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 0a907a02d8..e75353c7a5 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -63,6 +63,11 @@ class RenderCreator(Creator): "one composition." )) + for inst in self.create_context.instances: + if subset_name == inst.subset_name: + raise CreatorError("{} already exists".format( + inst.subset_name)) + data["members"] = [items[0].id] new_instance = CreatedInstance(self.family, subset_name, data, self) new_instance.creator_attributes["farm"] = pre_create_data["farm"] From 32f015098b95d7953d94d878f32afbd4022a18df Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 11:08:51 +0100 Subject: [PATCH 025/291] OP-2765 - reimplemented get_context_title --- openpype/hosts/aftereffects/api/pipeline.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 4ade90e4dd..38ab2225bf 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -273,7 +273,12 @@ def update_context_data(data, changes): def get_context_title(): """Returns title for Creator window""" - return "AfterEffects" + import avalon.api + + project_name = avalon.api.Session["AVALON_PROJECT"] + asset_name = avalon.api.Session["AVALON_ASSET"] + task_name = avalon.api.Session["AVALON_TASK"] + return "{}/{}/{}".format(project_name, asset_name, task_name) def _get_stub(): From 56e2121e308f6bdf7e1551336ae3c28104920775 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 15:23:46 +0100 Subject: [PATCH 026/291] OP-2765 - fix local rendering in old publish --- openpype/hosts/aftereffects/plugins/publish/collect_render.py | 4 ++-- .../hosts/aftereffects/plugins/publish/pre_collect_render.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index d31571b6b5..43efd34635 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -57,7 +57,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): compositions_by_id = {item.id: item for item in compositions} for inst in context: family = inst.data["family"] - if family != "render": + if family not in ["render", "renderLocal"]: # legacy continue item_id = inst.data["members"][0] @@ -123,7 +123,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): instance.comp_name = comp.name instance.comp_id = item_id - is_local = "renderLocal" in inst.data["families"] # legacy + is_local = "renderLocal" in inst.data["family"] # legacy if inst.data.get("creator_attributes"): is_local = not inst.data["creator_attributes"].get("farm") if is_local: diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py index 614a04b4b7..3e84753555 100644 --- a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -34,7 +34,7 @@ class PreCollectRender(pyblish.api.ContextPlugin): "Please recreate instance.") instance = context.create_instance(inst["subset"]) - inst["families"] = [self.family_remapping[inst["family"]]] + inst["families"] = [self.family_remapping[inst["family"]][0]] instance.data.update(inst) self._debug_log(instance) From ec9b4802f40d6fe1d3dd02ab1195bace33ef0c82 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 16:07:18 +0100 Subject: [PATCH 027/291] OP-2765 - trigger failure when new instance tried to be published by Pyblish This could happen if artist try to switch between old Pyblish and New Publish --- .../hosts/aftereffects/plugins/publish/pre_collect_render.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py index 3e84753555..46bb9865b9 100644 --- a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -26,6 +26,10 @@ class PreCollectRender(pyblish.api.ContextPlugin): return for inst in list_instances(): + if inst.get("creator_attributes"): + raise ValueError("Instance created in New publisher, " + "cannot be published in Pyblish") + if inst["family"] not in self.family_remapping.keys(): continue From a5c38a8b2f19d24c55c2be564ab701f68f886c36 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 16:24:59 +0100 Subject: [PATCH 028/291] OP-2765 - added new label for families In the future they will be both merged to render.farm (when Harmony is updated to New Publisher). --- openpype/lib/abstract_collect_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/lib/abstract_collect_render.py b/openpype/lib/abstract_collect_render.py index 3839aad45d..e160f5a040 100644 --- a/openpype/lib/abstract_collect_render.py +++ b/openpype/lib/abstract_collect_render.py @@ -138,7 +138,9 @@ class AbstractCollectRender(pyblish.api.ContextPlugin): try: if "workfile" in instance.data["families"]: instance.data["publish"] = True - if "renderFarm" in instance.data["families"]: + # TODO merge renderFarm and render.farm + if ("renderFarm" in instance.data["families"] or + "render.farm" in instance.data["families"]): instance.data["remove"] = True except KeyError: # be tolerant if 'families' is missing. From 3b9e319de27548a935b2aaba2064193a674fdd88 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 16:26:37 +0100 Subject: [PATCH 029/291] OP-2765 - fixed resolution between local and farm --- .../hosts/aftereffects/plugins/publish/collect_render.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 43efd34635..aa5bc58ac2 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -84,6 +84,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): subset_name = inst.data["subset"] instance = AERenderInstance( family=family, + families=inst.data.get("families", []), version=version, time="", source=current_file, @@ -130,7 +131,9 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): # for local renders instance = self._update_for_local(instance, project_entity) else: - instance.families = ["render.farm"] + fam = "render.farm" + if fam not in instance.families: + instance.families.append(fam) instances.append(instance) instances_to_remove.append(inst) @@ -210,7 +213,9 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): instance.anatomyData["subset"] = instance.subset instance.stagingDir = tempfile.mkdtemp() instance.projectEntity = project_entity - instance.families = ["render.local"] + fam = "render.local" + if fam not in instance.families: + instance.families.append(fam) settings = get_project_settings(os.getenv("AVALON_PROJECT")) reviewable_subset_filter = (settings["deadline"] From d4f50e2abdf55fed0c12f439062c75b5c780a7e3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Mar 2022 15:10:18 +0100 Subject: [PATCH 030/291] OP-2765 - fix imports for legacy farm creator --- .../aftereffects/plugins/create/create_legacy_render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py index 8dfc85cdc8..e4fbb47a33 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_legacy_render.py @@ -1,13 +1,12 @@ -from avalon.api import CreatorError - -import openpype.api +from openpype.pipeline import create +from openpype.pipeline import CreatorError from openpype.hosts.aftereffects.api import ( get_stub, list_instances ) -class CreateRender(openpype.api.Creator): +class CreateRender(create.LegacyCreator): """Render folder for publish. Creates subsets in format 'familyTaskSubsetname', @@ -23,6 +22,7 @@ class CreateRender(openpype.api.Creator): def process(self): stub = get_stub() # only after After Effects is up + items = [] if (self.options or {}).get("useSelection"): items = stub.get_selected_items( comps=True, folders=False, footages=False From a15552f878a0aab7ecfa37053ea2b646161cd37b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Mar 2022 15:10:42 +0100 Subject: [PATCH 031/291] OP-2765 - fix imports for new creator --- .../hosts/aftereffects/plugins/create/create_render.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index e75353c7a5..1a5a826137 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,7 +1,7 @@ -import avalon.api +from avalon import api as avalon_api from openpype import resources -import openpype.hosts.aftereffects.api as api +from openpype.hosts.aftereffects import api from openpype.pipeline import ( Creator, CreatedInstance, @@ -25,7 +25,7 @@ class RenderCreator(Creator): for instance_data in api.list_instances(): # legacy instances have family=='render' or 'renderLocal', use them creator_id = (instance_data.get("creator_identifier") or - instance_data.get("family").replace("Local", '')) + instance_data.get("family", '').replace("Local", '')) if creator_id == self.identifier: instance_data = self._handle_legacy(instance_data) instance = CreatedInstance.from_existing( @@ -108,7 +108,7 @@ class RenderCreator(Creator): instance_data.pop("uuid") if not instance_data.get("task"): - instance_data["task"] = avalon.api.Session.get("AVALON_TASK") + instance_data["task"] = avalon_api.Session.get("AVALON_TASK") if not instance_data.get("creator_attributes"): is_old_farm = instance_data["family"] != "renderLocal" From 60edd3abe6bf52271d7f1d84635f0be482d31c65 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Mar 2022 15:13:35 +0100 Subject: [PATCH 032/291] OP-2765 - added functionality to store/retrive context data These data is used for context publish information, for example storing enabling/disabling of validators. Currently not present in AE. --- openpype/hosts/aftereffects/api/pipeline.py | 22 +++++++++++++-------- openpype/hosts/aftereffects/api/ws_stub.py | 10 ++++++---- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 38ab2225bf..978d035020 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -9,6 +9,7 @@ from avalon import io, pipeline from openpype import lib from openpype.api import Logger +from openpype.pipeline import LegacyCreator import openpype.hosts.aftereffects from openpype.pipeline import BaseCreator @@ -34,7 +35,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.register_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) avalon.api.register_plugin_path(BaseCreator, CREATE_PATH) log.info(PUBLISH_PATH) @@ -48,7 +49,7 @@ def install(): def uninstall(): pyblish.api.deregister_plugin_path(PUBLISH_PATH) avalon.api.deregister_plugin_path(avalon.api.Loader, LOAD_PATH) - avalon.api.deregister_plugin_path(avalon.api.Creator, CREATE_PATH) + avalon.api.deregister_plugin_path(LegacyCreator, CREATE_PATH) def application_launch(): @@ -223,10 +224,8 @@ def list_instances(): layers_meta = stub.get_metadata() for instance in layers_meta: - if instance.get("schema") and \ - "container" in instance.get("schema"): - continue - instances.append(instance) + if instance.get("id") == "pyblish.avalon.instance": + instances.append(instance) return instances @@ -263,12 +262,19 @@ def remove_instance(instance): # new publisher section def get_context_data(): - print("get_context_data") + meta = _get_stub().get_metadata() + for item in meta: + if item.get("id") == "publish_context": + item.pop("id") + return item + return {} def update_context_data(data, changes): - print("update_context_data") + item = data + item["id"] = "publish_context" + _get_stub().imprint(item["id"], item) def get_context_title(): diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 1d3b69e038..d2dc40ec89 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -155,10 +155,12 @@ class AfterEffectsServerStub(): item_ids = [int(item.id) for item in all_items] cleaned_data = [] for meta in result_meta: - # for creation of instance OR loaded container - if 'instance' in meta.get('id') or \ - int(meta.get('members')[0]) in item_ids: - cleaned_data.append(meta) + # do not added instance with nonexistend item id + if meta.get("members"): + if int(meta["members"][0]) not in item_ids: + continue + + cleaned_data.append(meta) payload = json.dumps(cleaned_data, indent=4) From 3b4f96efa601351bb894f64a6e3d2d2e2c55d88b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 10 Mar 2022 15:19:42 +0100 Subject: [PATCH 033/291] OP-2765 - more explicit error message --- .../hosts/aftereffects/plugins/publish/pre_collect_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py index 46bb9865b9..03ec184524 100644 --- a/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/pre_collect_render.py @@ -28,7 +28,9 @@ class PreCollectRender(pyblish.api.ContextPlugin): for inst in list_instances(): if inst.get("creator_attributes"): raise ValueError("Instance created in New publisher, " - "cannot be published in Pyblish") + "cannot be published in Pyblish.\n" + "Please publish in New Publisher " + "or recreate instances with legacy Creators") if inst["family"] not in self.family_remapping.keys(): continue From 65b00455614cadd5f279fcfdd37c41f976697c99 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Mar 2022 17:31:57 +0100 Subject: [PATCH 034/291] OP-2766 - fixed not working self.log in New Publisher --- openpype/pipeline/create/creator_plugins.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/pipeline/create/creator_plugins.py b/openpype/pipeline/create/creator_plugins.py index 1ac2c420a2..f05b132fc6 100644 --- a/openpype/pipeline/create/creator_plugins.py +++ b/openpype/pipeline/create/creator_plugins.py @@ -69,7 +69,9 @@ class BaseCreator: @property def log(self): if self._log is None: - self._log = logging.getLogger(self.__class__.__name__) + from openpype.api import Logger + + self._log = Logger.get_logger(self.__class__.__name__) return self._log def _add_instance_to_context(self, instance): From a71dad4608e0be4a91c75769e5edf6722f52f9ff Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Mar 2022 17:35:17 +0100 Subject: [PATCH 035/291] OP-2766 - implemented auto creator for PS Creates workfile instance, updated imprint function. --- openpype/hosts/photoshop/api/pipeline.py | 52 +++++++++---- openpype/hosts/photoshop/api/ws_stub.py | 33 +++++---- .../plugins/create/workfile_creator.py | 73 +++++++++++++++++++ 3 files changed, 131 insertions(+), 27 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/create/workfile_creator.py diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 1be8129aa1..0e3f1215aa 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -8,7 +8,7 @@ from avalon import pipeline, io from openpype.api import Logger from openpype.lib import register_event_callback -from openpype.pipeline import LegacyCreator +from openpype.pipeline import LegacyCreator, BaseCreator import openpype.hosts.photoshop from . import lib @@ -71,6 +71,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) avalon.api.register_plugin_path(avalon.api.Loader, LOAD_PATH) avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) + avalon.api.register_plugin_path(BaseCreator, CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( @@ -144,12 +145,9 @@ def list_instances(): layers_meta = stub.get_layers_metadata() if layers_meta: for key, instance in layers_meta.items(): - schema = instance.get("schema") - if schema and "container" in schema: - continue - - instance['uuid'] = key - instances.append(instance) + if instance.get("id") == "pyblish.avalon.instance": # TODO only this way? + instance['uuid'] = key + instances.append(instance) return instances @@ -170,11 +168,18 @@ def remove_instance(instance): if not stub: return - stub.remove_instance(instance.get("uuid")) - layer = stub.get_layer(instance.get("uuid")) - if layer: - stub.rename_layer(instance.get("uuid"), - layer.name.replace(stub.PUBLISH_ICON, '')) + inst_id = instance.get("instance_id") or instance.get("uuid") # legacy + if not inst_id: + log.warning("No instance identifier for {}".format(instance)) + return + + stub.remove_instance(inst_id) + + if instance.get("members"): + item = stub.get_item(instance["members"][0]) + if item: + stub.rename_item(item.id, + item.name.replace(stub.PUBLISH_ICON, '')) def _get_stub(): @@ -226,6 +231,27 @@ def containerise( "members": [str(layer.id)] } stub = lib.stub() - stub.imprint(layer, data) + stub.imprint(layer.id, data) return layer + + +def get_context_data(): + pass + + +def update_context_data(data, changes): + # item = data + # item["id"] = "publish_context" + # _get_stub().imprint(item["id"], item) + pass + + +def get_context_title(): + """Returns title for Creator window""" + import avalon.api + + project_name = avalon.api.Session["AVALON_PROJECT"] + asset_name = avalon.api.Session["AVALON_ASSET"] + task_name = avalon.api.Session["AVALON_TASK"] + return "{}/{}/{}".format(project_name, asset_name, task_name) \ No newline at end of file diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index 64d89f5420..a99f184080 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -27,6 +27,7 @@ class PSItem(object): members = attr.ib(factory=list) long_name = attr.ib(default=None) color_code = attr.ib(default=None) # color code of layer + instance_id = attr.ib(default=None) class PhotoshopServerStub: @@ -82,7 +83,7 @@ class PhotoshopServerStub: return layers_meta.get(str(layer.id)) - def imprint(self, layer, data, all_layers=None, layers_meta=None): + def imprint(self, item_id, data, all_layers=None, items_meta=None): """Save layer metadata to Headline field of active document Stores metadata in format: @@ -108,28 +109,29 @@ class PhotoshopServerStub: }] - for loaded instances Args: - layer (PSItem): + item_id (str): data(string): json representation for single layer all_layers (list of PSItem): for performance, could be injected for usage in loop, if not, single call will be triggered - layers_meta(string): json representation from Headline + items_meta(string): json representation from Headline (for performance - provide only if imprint is in loop - value should be same) Returns: None """ - if not layers_meta: - layers_meta = self.get_layers_metadata() + if not items_meta: + items_meta = self.get_layers_metadata() # json.dumps writes integer values in a dictionary to string, so # anticipating it here. - if str(layer.id) in layers_meta and layers_meta[str(layer.id)]: + item_id = str(item_id) + if item_id in items_meta.keys(): if data: - layers_meta[str(layer.id)].update(data) + items_meta[item_id].update(data) else: - layers_meta.pop(str(layer.id)) + items_meta.pop(item_id) else: - layers_meta[str(layer.id)] = data + items_meta[item_id] = data # Ensure only valid ids are stored. if not all_layers: @@ -137,12 +139,14 @@ class PhotoshopServerStub: layer_ids = [layer.id for layer in all_layers] cleaned_data = [] - for layer_id in layers_meta: - if int(layer_id) in layer_ids: - cleaned_data.append(layers_meta[layer_id]) + for item in items_meta.values(): + if item.get("members"): + if int(item["members"][0]) not in layer_ids: + continue + + cleaned_data.append(item) payload = json.dumps(cleaned_data, indent=4) - self.websocketserver.call( self.client.call('Photoshop.imprint', payload=payload) ) @@ -528,6 +532,7 @@ class PhotoshopServerStub: d.get('type'), d.get('members'), d.get('long_name'), - d.get("color_code") + d.get("color_code"), + d.get("instance_id") )) return ret diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py new file mode 100644 index 0000000000..d66a05cad7 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -0,0 +1,73 @@ +from avalon import io + +import openpype.hosts.photoshop.api as api +from openpype.pipeline import ( + AutoCreator, + CreatedInstance +) + + +class PSWorkfileCreator(AutoCreator): + identifier = "workfile" + family = "workfile" + + def get_instance_attr_defs(self): + return [] + + def collect_instances(self): + for instance_data in api.list_instances(): + creator_id = instance_data.get("creator_identifier") + if creator_id == self.identifier: + subset_name = instance_data["subset"] + instance = CreatedInstance( + self.family, subset_name, instance_data, self + ) + self._add_instance_to_context(instance) + + def update_instances(self, update_list): + # nothing to change on workfiles + pass + + def create(self, options=None): + existing_instance = None + for instance in self.create_context.instances: + if instance.family == self.family: + existing_instance = instance + break + + variant = '' + project_name = io.Session["AVALON_PROJECT"] + asset_name = io.Session["AVALON_ASSET"] + task_name = io.Session["AVALON_TASK"] + host_name = io.Session["AVALON_APP"] + if existing_instance is None: + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + data = { + "asset": asset_name, + "task": task_name, + "variant": variant + } + data.update(self.get_dynamic_data( + variant, task_name, asset_doc, project_name, host_name + )) + + new_instance = CreatedInstance( + self.family, subset_name, data, self + ) + self._add_instance_to_context(new_instance) + api.stub().imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + + elif ( + existing_instance["asset"] != asset_name + or existing_instance["task"] != task_name + ): + asset_doc = io.find_one({"type": "asset", "name": asset_name}) + subset_name = self.get_subset_name( + variant, task_name, asset_doc, project_name, host_name + ) + existing_instance["asset"] = asset_name + existing_instance["task"] = task_name From cdb2047ef7e205054f2c31fb6f336e259fa93d47 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 14 Mar 2022 17:35:40 +0100 Subject: [PATCH 036/291] OP-2766 - renamed legacy creator --- .../plugins/create/create_legacy_image.py | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 openpype/hosts/photoshop/plugins/create/create_legacy_image.py diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py new file mode 100644 index 0000000000..a001b5f171 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -0,0 +1,99 @@ +from Qt import QtWidgets +from openpype.pipeline import create +from openpype.hosts.photoshop import api as photoshop + + +class CreateImage(create.LegacyCreator): + """Image folder for publish.""" + + name = "imageDefault" + label = "Image" + family = "image" + defaults = ["Main"] + + def process(self): + groups = [] + layers = [] + create_group = False + + stub = photoshop.stub() + if (self.options or {}).get("useSelection"): + multiple_instances = False + selection = stub.get_selected_layers() + self.log.info("selection {}".format(selection)) + if len(selection) > 1: + # Ask user whether to create one image or image per selected + # item. + msg_box = QtWidgets.QMessageBox() + msg_box.setIcon(QtWidgets.QMessageBox.Warning) + msg_box.setText( + "Multiple layers selected." + "\nDo you want to make one image per layer?" + ) + msg_box.setStandardButtons( + QtWidgets.QMessageBox.Yes | + QtWidgets.QMessageBox.No | + QtWidgets.QMessageBox.Cancel + ) + ret = msg_box.exec_() + if ret == QtWidgets.QMessageBox.Yes: + multiple_instances = True + elif ret == QtWidgets.QMessageBox.Cancel: + return + + if multiple_instances: + for item in selection: + if item.group: + groups.append(item) + else: + layers.append(item) + else: + group = stub.group_selected_layers(self.name) + groups.append(group) + + elif len(selection) == 1: + # One selected item. Use group if its a LayerSet (group), else + # create a new group. + if selection[0].group: + groups.append(selection[0]) + else: + layers.append(selection[0]) + elif len(selection) == 0: + # No selection creates an empty group. + create_group = True + else: + group = stub.create_group(self.name) + groups.append(group) + + if create_group: + group = stub.create_group(self.name) + groups.append(group) + + for layer in layers: + stub.select_layers([layer]) + group = stub.group_selected_layers(layer.name) + groups.append(group) + + creator_subset_name = self.data["subset"] + for group in groups: + long_names = [] + group.name = group.name.replace(stub.PUBLISH_ICON, ''). \ + replace(stub.LOADED_ICON, '') + + subset_name = creator_subset_name + if len(groups) > 1: + subset_name += group.name.title().replace(" ", "") + + if group.long_name: + for directory in group.long_name[::-1]: + name = directory.replace(stub.PUBLISH_ICON, '').\ + replace(stub.LOADED_ICON, '') + long_names.append(name) + + self.data.update({"subset": subset_name}) + self.data.update({"uuid": str(group.id)}) + self.data.update({"long_name": "_".join(long_names)}) + stub.imprint(group, self.data) + # reusing existing group, need to rename afterwards + if not create_group: + stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) From bfce93027ccd5ebbb227b7af80ba8d73c77f3453 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Mar 2022 15:00:17 +0100 Subject: [PATCH 037/291] Update openpype/hosts/aftereffects/plugins/create/create_render.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/aftereffects/plugins/create/create_render.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 1a5a826137..550fb6b0ef 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -72,7 +72,7 @@ class RenderCreator(Creator): new_instance = CreatedInstance(self.family, subset_name, data, self) new_instance.creator_attributes["farm"] = pre_create_data["farm"] - api.get_stub().imprint(new_instance.get("instance_id"), + api.get_stub().imprint(new_instance.id, new_instance.data_to_store()) self._add_instance_to_context(new_instance) From d3441215749e303311370a41a9c82aa934b6cfb0 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Mar 2022 15:00:33 +0100 Subject: [PATCH 038/291] Update openpype/hosts/aftereffects/plugins/create/create_render.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/hosts/aftereffects/plugins/create/create_render.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 550fb6b0ef..88462667ed 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -70,7 +70,9 @@ class RenderCreator(Creator): data["members"] = [items[0].id] new_instance = CreatedInstance(self.family, subset_name, data, self) - new_instance.creator_attributes["farm"] = pre_create_data["farm"] + if "farm" in pre_create_data: + use_farm = pre_create_data["farm"] + new_instance.creator_attributes["farm"] = use_farm api.get_stub().imprint(new_instance.id, new_instance.data_to_store()) From bff1b77c0635493c3236f663c7a444eaf2d350e4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Mar 2022 16:17:46 +0100 Subject: [PATCH 039/291] OP-2766 - changed format of layer metadata Removing uuid, replaced with members[0] and instance_id. Layers metadata now returned as a list, not dictionary to follow AE implementation. --- openpype/hosts/photoshop/api/pipeline.py | 3 +- openpype/hosts/photoshop/api/ws_stub.py | 60 ++++++++++++------------ 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 0e3f1215aa..8d64942c9e 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -144,9 +144,8 @@ def list_instances(): instances = [] layers_meta = stub.get_layers_metadata() if layers_meta: - for key, instance in layers_meta.items(): + for instance in layers_meta: if instance.get("id") == "pyblish.avalon.instance": # TODO only this way? - instance['uuid'] = key instances.append(instance) return instances diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index a99f184080..dd29ef4e84 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -81,7 +81,11 @@ class PhotoshopServerStub: if layers_meta is None: layers_meta = self.get_layers_metadata() - return layers_meta.get(str(layer.id)) + for layer_meta in layers_meta: + if layer_meta.get("members"): + if layer.id == layer_meta["members"][0]: + return layer + print("Unable to find layer metadata for {}".format(layer.id)) def imprint(self, item_id, data, all_layers=None, items_meta=None): """Save layer metadata to Headline field of active document @@ -125,13 +129,21 @@ class PhotoshopServerStub: # json.dumps writes integer values in a dictionary to string, so # anticipating it here. item_id = str(item_id) - if item_id in items_meta.keys(): - if data: - items_meta[item_id].update(data) + is_new = True + result_meta = [] + for item_meta in items_meta: + if ((item_meta.get('members') and + item_id == str(item_meta.get('members')[0])) or + item_meta.get("instance_id") == item_id): + is_new = False + if data: + item_meta.update(data) + result_meta.append(item_meta) else: - items_meta.pop(item_id) - else: - items_meta[item_id] = data + result_meta.append(item_meta) + + if is_new: + result_meta.append(data) # Ensure only valid ids are stored. if not all_layers: @@ -139,7 +151,7 @@ class PhotoshopServerStub: layer_ids = [layer.id for layer in all_layers] cleaned_data = [] - for item in items_meta.values(): + for item in result_meta: if item.get("members"): if int(item["members"][0]) not in layer_ids: continue @@ -374,38 +386,27 @@ class PhotoshopServerStub: (Headline accessible by File > File Info) Returns: - (string): - json documents + (list) example: {"8":{"active":true,"subset":"imageBG", "family":"image","id":"pyblish.avalon.instance", "asset":"Town"}} 8 is layer(group) id - used for deletion, update etc. """ - layers_data = {} res = self.websocketserver.call(self.client.call('Photoshop.read')) + layers_data = [] try: - layers_data = json.loads(res) + if res: + layers_data = json.loads(res) except json.decoder.JSONDecodeError: pass # format of metadata changed from {} to [] because of standardization # keep current implementation logic as its working - if not isinstance(layers_data, dict): - temp_layers_meta = {} - for layer_meta in layers_data: - layer_id = layer_meta.get("uuid") - if not layer_id: - layer_id = layer_meta.get("members")[0] - - temp_layers_meta[layer_id] = layer_meta - layers_data = temp_layers_meta - else: - # legacy version of metadata + if isinstance(layers_data, dict): for layer_id, layer_meta in layers_data.items(): if layer_meta.get("schema") != "openpype:container-2.0": - layer_meta["uuid"] = str(layer_id) - else: layer_meta["members"] = [str(layer_id)] - + layers_data = list(layers_data.values()) return layers_data def import_smart_object(self, path, layer_name, as_reference=False): @@ -476,11 +477,12 @@ class PhotoshopServerStub: ) def remove_instance(self, instance_id): - cleaned_data = {} + cleaned_data = [] - for key, instance in self.get_layers_metadata().items(): - if key != instance_id: - cleaned_data[key] = instance + for item in self.get_layers_metadata(): + inst_id = item.get("instance_id") or item.get("uuid") + if inst_id != instance_id: + cleaned_data.append(inst_id) payload = json.dumps(cleaned_data, indent=4) From c46b41804d108cc976aae64410ce520ac3117dda Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Mar 2022 16:18:07 +0100 Subject: [PATCH 040/291] OP-2766 - implemented new image Creator Working implementation of New Publisher (not full backward compatibility yet). --- openpype/hosts/photoshop/api/__init__.py | 8 +- .../photoshop/plugins/create/create_image.py | 156 ++++++++++++------ .../plugins/create/create_legacy_image.py | 2 +- .../plugins/create/workfile_creator.py | 2 + .../plugins/publish/collect_instances.py | 4 + .../plugins/publish/collect_workfile.py | 30 ++-- .../plugins/publish/extract_image.py | 9 +- 7 files changed, 148 insertions(+), 63 deletions(-) diff --git a/openpype/hosts/photoshop/api/__init__.py b/openpype/hosts/photoshop/api/__init__.py index 17ea957066..94152b5706 100644 --- a/openpype/hosts/photoshop/api/__init__.py +++ b/openpype/hosts/photoshop/api/__init__.py @@ -12,7 +12,10 @@ from .pipeline import ( remove_instance, install, uninstall, - containerise + containerise, + get_context_data, + update_context_data, + get_context_title ) from .plugin import ( PhotoshopLoader, @@ -43,6 +46,9 @@ __all__ = [ "install", "uninstall", "containerise", + "get_context_data", + "update_context_data", + "get_context_title", # Plugin "PhotoshopLoader", diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index a001b5f171..a73b79e0fd 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,46 +1,50 @@ -from Qt import QtWidgets -from openpype.pipeline import create -from openpype.hosts.photoshop import api as photoshop +from avalon import api as avalon_api +from openpype.hosts.photoshop import api +from openpype.pipeline import ( + Creator, + CreatedInstance, + lib, + CreatorError +) -class CreateImage(create.LegacyCreator): - """Image folder for publish.""" - - name = "imageDefault" +class ImageCreator(Creator): + """Creates image instance for publishing.""" + identifier = "image" label = "Image" family = "image" - defaults = ["Main"] + description = "Image creator" - def process(self): + def collect_instances(self): + import json + self.log.info("ImageCreator: api.list_instances():: {}".format( + json.dumps(api.list_instances(), indent=4))) + for instance_data in api.list_instances(): + # legacy instances have family=='image' + creator_id = (instance_data.get("creator_identifier") or + instance_data.get("family")) + + self.log.info("ImageCreator: instance_data:: {}".format(json.dumps(instance_data, indent=4))) + if creator_id == self.identifier: + instance_data = self._handle_legacy(instance_data) + + layer = api.stub().get_layer(instance_data["members"][0]) + instance_data["layer"] = layer + instance = CreatedInstance.from_existing( + instance_data, self + ) + self._add_instance_to_context(instance) + + def create(self, subset_name, data, pre_create_data): groups = [] layers = [] create_group = False - stub = photoshop.stub() - if (self.options or {}).get("useSelection"): - multiple_instances = False - selection = stub.get_selected_layers() - self.log.info("selection {}".format(selection)) + stub = api.stub() # only after PS is up + multiple_instances = pre_create_data.get("create_multiple") + selection = stub.get_selected_layers() + if pre_create_data.get("use_selection"): if len(selection) > 1: - # Ask user whether to create one image or image per selected - # item. - msg_box = QtWidgets.QMessageBox() - msg_box.setIcon(QtWidgets.QMessageBox.Warning) - msg_box.setText( - "Multiple layers selected." - "\nDo you want to make one image per layer?" - ) - msg_box.setStandardButtons( - QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No | - QtWidgets.QMessageBox.Cancel - ) - ret = msg_box.exec_() - if ret == QtWidgets.QMessageBox.Yes: - multiple_instances = True - elif ret == QtWidgets.QMessageBox.Cancel: - return - if multiple_instances: for item in selection: if item.group: @@ -48,25 +52,25 @@ class CreateImage(create.LegacyCreator): else: layers.append(item) else: - group = stub.group_selected_layers(self.name) + group = stub.group_selected_layers(subset_name) groups.append(group) - elif len(selection) == 1: # One selected item. Use group if its a LayerSet (group), else # create a new group. - if selection[0].group: - groups.append(selection[0]) + selected_item = selection[0] + if selected_item.group: + groups.append(selected_item) else: - layers.append(selection[0]) + layers.append(selected_item) elif len(selection) == 0: # No selection creates an empty group. create_group = True else: - group = stub.create_group(self.name) + group = stub.create_group(subset_name) groups.append(group) if create_group: - group = stub.create_group(self.name) + group = stub.create_group(subset_name) groups.append(group) for layer in layers: @@ -74,26 +78,78 @@ class CreateImage(create.LegacyCreator): group = stub.group_selected_layers(layer.name) groups.append(group) - creator_subset_name = self.data["subset"] for group in groups: long_names = [] - group.name = group.name.replace(stub.PUBLISH_ICON, ''). \ - replace(stub.LOADED_ICON, '') + group.name = self._clean_highlights(stub, group.name) - subset_name = creator_subset_name if len(groups) > 1: subset_name += group.name.title().replace(" ", "") if group.long_name: for directory in group.long_name[::-1]: - name = directory.replace(stub.PUBLISH_ICON, '').\ - replace(stub.LOADED_ICON, '') + name = self._clean_highlights(stub, directory) long_names.append(name) - self.data.update({"subset": subset_name}) - self.data.update({"uuid": str(group.id)}) - self.data.update({"long_name": "_".join(long_names)}) - stub.imprint(group, self.data) + data.update({"subset": subset_name}) + data.update({"layer": group}) + data.update({"members": [str(group.id)]}) + data.update({"long_name": "_".join(long_names)}) + + new_instance = CreatedInstance(self.family, subset_name, data, + self) + + stub.imprint(new_instance.get("instance_id"), + new_instance.data_to_store()) + self._add_instance_to_context(new_instance) # reusing existing group, need to rename afterwards if not create_group: stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) + + def update_instances(self, update_list): + self.log.info("update_list:: {}".format(update_list)) + created_inst, changes = update_list[0] + api.stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) + + def remove_instances(self, instances): + for instance in instances: + api.remove_instance(instance) + self._remove_instance_from_context(instance) + + def get_default_variants(self): + return [ + "Main" + ] + + def get_pre_create_attr_defs(self): + output = [ + lib.BoolDef("use_selection", default=True, label="Use selection"), + lib.BoolDef("create_multiple", + default=True, + label="Create separate instance for each selected") + ] + return output + + def get_detail_description(self): + return """Creator for Image instances""" + + def _handle_legacy(self, instance_data): + """Converts old instances to new format.""" + if not instance_data.get("members"): + instance_data["members"] = [instance_data.get("uuid")] + + if instance_data.get("uuid"): + # uuid not needed, replaced with unique instance_id + api.stub().remove_instance(instance_data.get("uuid")) + instance_data.pop("uuid") + + if not instance_data.get("task"): + instance_data["task"] = avalon_api.Session.get("AVALON_TASK") + + return instance_data + + def _clean_highlights(self, stub, item): + return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON, + '') + + diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index a001b5f171..6fa455fa03 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -91,7 +91,7 @@ class CreateImage(create.LegacyCreator): long_names.append(name) self.data.update({"subset": subset_name}) - self.data.update({"uuid": str(group.id)}) + self.data.update({"members": [str(group.id)]}) self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) # reusing existing group, need to rename afterwards diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py index d66a05cad7..2a2fda3cc4 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -15,6 +15,7 @@ class PSWorkfileCreator(AutoCreator): return [] def collect_instances(self): + print("coll::{}".format(api.list_instances())) for instance_data in api.list_instances(): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: @@ -29,6 +30,7 @@ class PSWorkfileCreator(AutoCreator): pass def create(self, options=None): + print("create") existing_instance = None for instance in self.create_context.instances: if instance.family == self.family: diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index c3e27e9646..ee402dcabf 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -21,6 +21,10 @@ class CollectInstances(pyblish.api.ContextPlugin): } def process(self, context): + if context.data.get("newPublishing"): + self.log.debug("Not applicable for New Publisher, skip") + return + stub = photoshop.stub() layers = stub.get_layers() layers_meta = stub.get_layers_metadata() diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index db1ede14d5..bdbd379a33 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -10,6 +10,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): hosts = ["photoshop"] def process(self, context): + existing_instance = None + for instance in context: + if instance.data["family"] == "workfile": + self.log.debug("Workfile instance found, won't create new") + existing_instance = instance + break + family = "workfile" task = os.getenv("AVALON_TASK", None) subset = family + task.capitalize() @@ -19,16 +26,19 @@ class CollectWorkfile(pyblish.api.ContextPlugin): base_name = os.path.basename(file_path) # Create instance - instance = context.create_instance(subset) - instance.data.update({ - "subset": subset, - "label": base_name, - "name": base_name, - "family": family, - "families": [], - "representations": [], - "asset": os.environ["AVALON_ASSET"] - }) + if existing_instance is None: + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": base_name, + "name": base_name, + "family": family, + "families": [], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) + else: + instance = existing_instance # creating representation _, ext = os.path.splitext(file_path) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index 04ce77ee34..d27c5bc028 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -16,7 +16,8 @@ class ExtractImage(openpype.api.Extractor): formats = ["png", "jpg"] def process(self, instance): - + print("PPPPPP") + self.log.info("fdfdsfdfs") staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) @@ -26,7 +27,13 @@ class ExtractImage(openpype.api.Extractor): with photoshop.maintained_selection(): self.log.info("Extracting %s" % str(list(instance))) with photoshop.maintained_visibility(): + self.log.info("instance.data:: {}".format(instance.data)) + print("instance.data::: {}".format(instance.data)) layer = instance.data.get("layer") + self.log.info("layer:: {}".format(layer)) + print("layer::: {}".format(layer)) + if not layer: + return ids = set([layer.id]) add_ids = instance.data.pop("ids", None) if add_ids: From a5ac3ab55b2c67604ef8e2530c57bdf242e6c599 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 16 Mar 2022 16:21:29 +0100 Subject: [PATCH 041/291] OP-2766 - implemented new context methods --- openpype/hosts/photoshop/api/pipeline.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 8d64942c9e..0a99d1779d 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -236,14 +236,21 @@ def containerise( def get_context_data(): - pass + """Get stored values for context (validation enable/disable etc)""" + meta = _get_stub().get_layers_metadata() + for item in meta: + if item.get("id") == "publish_context": + item.pop("id") + return item + + return {} def update_context_data(data, changes): - # item = data - # item["id"] = "publish_context" - # _get_stub().imprint(item["id"], item) - pass + """Store value needed for context""" + item = data + item["id"] = "publish_context" + _get_stub().imprint(item["id"], item) def get_context_title(): From df5fdcc54c6ff125d307036b26a07572671047c9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Mar 2022 10:45:54 +0100 Subject: [PATCH 042/291] OP-2766 - do not store PSItem in metadata PSItem is not serializable --- openpype/hosts/photoshop/plugins/create/create_image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index a73b79e0fd..4fc9a86635 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -108,6 +108,7 @@ class ImageCreator(Creator): def update_instances(self, update_list): self.log.info("update_list:: {}".format(update_list)) created_inst, changes = update_list[0] + created_inst.pop("layer") # not storing PSItem layer to metadata api.stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) From c422176553ff27cff8d5113958fadf0dc4ddf12e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 17 Mar 2022 11:29:41 +0100 Subject: [PATCH 043/291] OP-2766 - removed hardcoded ftrack, CollectFtrackFamily should be used Added defaults for Ftrack Settings. --- .../plugins/publish/collect_review.py | 25 +++++++++++++------ .../defaults/project_settings/ftrack.json | 12 +++++++++ 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 5ab48b76da..4b6f855a6a 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -1,12 +1,24 @@ +""" +Requires: + None + +Provides: + instance -> family ("review") +""" + import os import pyblish.api class CollectReview(pyblish.api.ContextPlugin): - """Gather the active document as review instance.""" + """Gather the active document as review instance. - label = "Review" + Triggers once even if no 'image' is published as by defaults it creates + flatten image from a workfile. + """ + + label = "Collect Review" order = pyblish.api.CollectorOrder hosts = ["photoshop"] @@ -15,16 +27,13 @@ class CollectReview(pyblish.api.ContextPlugin): task = os.getenv("AVALON_TASK", None) subset = family + task.capitalize() - file_path = context.data["currentFile"] - base_name = os.path.basename(file_path) - instance = context.create_instance(subset) instance.data.update({ "subset": subset, - "label": base_name, - "name": base_name, + "label": subset, + "name": subset, "family": family, - "families": ["ftrack"], + "families": [], "representations": [], "asset": os.environ["AVALON_ASSET"] }) diff --git a/openpype/settings/defaults/project_settings/ftrack.json b/openpype/settings/defaults/project_settings/ftrack.json index 01831efad1..015413e64f 100644 --- a/openpype/settings/defaults/project_settings/ftrack.json +++ b/openpype/settings/defaults/project_settings/ftrack.json @@ -344,6 +344,18 @@ "tasks": [], "add_ftrack_family": true, "advanced_filtering": [] + }, + { + "hosts": [ + "photoshop" + ], + "families": [ + "review" + ], + "task_types": [], + "tasks": [], + "add_ftrack_family": true, + "advanced_filtering": [] } ] }, From a6a1d0fc545d8fc5a8781f40468a95a261ca3b01 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Mar 2022 15:22:46 +0100 Subject: [PATCH 044/291] OP-2766 - fixed broken remove_instance --- openpype/hosts/photoshop/api/ws_stub.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/api/ws_stub.py b/openpype/hosts/photoshop/api/ws_stub.py index dd29ef4e84..fa076ecc7e 100644 --- a/openpype/hosts/photoshop/api/ws_stub.py +++ b/openpype/hosts/photoshop/api/ws_stub.py @@ -77,14 +77,28 @@ class PhotoshopServerStub: layer: (PSItem) layers_meta: full list from Headline (for performance in loops) Returns: + (dict) of layer metadata stored in PS file + + Example: + { + 'id': 'pyblish.avalon.container', + 'loader': 'ImageLoader', + 'members': ['64'], + 'name': 'imageMainMiddle', + 'namespace': 'Hero_imageMainMiddle_001', + 'representation': '6203dc91e80934d9f6ee7d96', + 'schema': 'openpype:container-2.0' + } """ if layers_meta is None: layers_meta = self.get_layers_metadata() for layer_meta in layers_meta: + layer_id = layer_meta.get("uuid") # legacy if layer_meta.get("members"): - if layer.id == layer_meta["members"][0]: - return layer + layer_id = layer_meta["members"][0] + if str(layer.id) == str(layer_id): + return layer_meta print("Unable to find layer metadata for {}".format(layer.id)) def imprint(self, item_id, data, all_layers=None, items_meta=None): @@ -399,7 +413,7 @@ class PhotoshopServerStub: if res: layers_data = json.loads(res) except json.decoder.JSONDecodeError: - pass + raise ValueError("{} cannot be parsed, recreate meta".format(res)) # format of metadata changed from {} to [] because of standardization # keep current implementation logic as its working if isinstance(layers_data, dict): @@ -482,7 +496,7 @@ class PhotoshopServerStub: for item in self.get_layers_metadata(): inst_id = item.get("instance_id") or item.get("uuid") if inst_id != instance_id: - cleaned_data.append(inst_id) + cleaned_data.append(item) payload = json.dumps(cleaned_data, indent=4) From 01f2c8c1044ddeb78912dc2f6e401a4700e1a67d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 18 Mar 2022 15:24:51 +0100 Subject: [PATCH 045/291] OP-2766 - fixed layer and variant keys --- .../hosts/photoshop/plugins/create/create_image.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 4fc9a86635..c24d8bde2f 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -16,18 +16,13 @@ class ImageCreator(Creator): description = "Image creator" def collect_instances(self): - import json - self.log.info("ImageCreator: api.list_instances():: {}".format( - json.dumps(api.list_instances(), indent=4))) for instance_data in api.list_instances(): # legacy instances have family=='image' creator_id = (instance_data.get("creator_identifier") or instance_data.get("family")) - self.log.info("ImageCreator: instance_data:: {}".format(json.dumps(instance_data, indent=4))) if creator_id == self.identifier: instance_data = self._handle_legacy(instance_data) - layer = api.stub().get_layer(instance_data["members"][0]) instance_data["layer"] = layer instance = CreatedInstance.from_existing( @@ -106,9 +101,10 @@ class ImageCreator(Creator): stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) def update_instances(self, update_list): - self.log.info("update_list:: {}".format(update_list)) + self.log.debug("update_list:: {}".format(update_list)) created_inst, changes = update_list[0] - created_inst.pop("layer") # not storing PSItem layer to metadata + if created_inst.get("layer"): + created_inst.pop("layer") # not storing PSItem layer to metadata api.stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) @@ -147,6 +143,9 @@ class ImageCreator(Creator): if not instance_data.get("task"): instance_data["task"] = avalon_api.Session.get("AVALON_TASK") + if not instance_data.get("variant"): + instance_data["variant"] = '' + return instance_data def _clean_highlights(self, stub, item): From b71554fe25375af9e87b7c854d3492d9f932de02 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 22 Mar 2022 14:56:29 +0100 Subject: [PATCH 046/291] OP-2765 - fix for update of multiple instances --- openpype/hosts/aftereffects/plugins/create/create_render.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 1a5a826137..e4f1f57b84 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -34,9 +34,9 @@ class RenderCreator(Creator): self._add_instance_to_context(instance) def update_instances(self, update_list): - created_inst, changes = update_list[0] - api.get_stub().imprint(created_inst.get("instance_id"), - created_inst.data_to_store()) + for created_inst, _changes in update_list: + api.get_stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) def remove_instances(self, instances): for instance in instances: From 6fde2110148e62649ae3bd0d25726d5dd9c16859 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Tue, 22 Mar 2022 16:37:31 +0100 Subject: [PATCH 047/291] OP-2766 - fix loaders because of change in imprint signature --- openpype/hosts/photoshop/plugins/load/load_image.py | 4 ++-- openpype/hosts/photoshop/plugins/load/load_reference.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/load/load_image.py b/openpype/hosts/photoshop/plugins/load/load_image.py index 0a9421b8f2..91a9787781 100644 --- a/openpype/hosts/photoshop/plugins/load/load_image.py +++ b/openpype/hosts/photoshop/plugins/load/load_image.py @@ -61,7 +61,7 @@ class ImageLoader(photoshop.PhotoshopLoader): ) stub.imprint( - layer, {"representation": str(representation["_id"])} + layer.id, {"representation": str(representation["_id"])} ) def remove(self, container): @@ -73,7 +73,7 @@ class ImageLoader(photoshop.PhotoshopLoader): stub = self.get_stub() layer = container.pop("layer") - stub.imprint(layer, {}) + stub.imprint(layer.id, {}) stub.delete_layer(layer.id) def switch(self, container, representation): diff --git a/openpype/hosts/photoshop/plugins/load/load_reference.py b/openpype/hosts/photoshop/plugins/load/load_reference.py index f5f0545d39..1f32a5d23c 100644 --- a/openpype/hosts/photoshop/plugins/load/load_reference.py +++ b/openpype/hosts/photoshop/plugins/load/load_reference.py @@ -61,7 +61,7 @@ class ReferenceLoader(photoshop.PhotoshopLoader): ) stub.imprint( - layer, {"representation": str(representation["_id"])} + layer.id, {"representation": str(representation["_id"])} ) def remove(self, container): @@ -72,7 +72,7 @@ class ReferenceLoader(photoshop.PhotoshopLoader): """ stub = self.get_stub() layer = container.pop("layer") - stub.imprint(layer, {}) + stub.imprint(layer.id, {}) stub.delete_layer(layer.id) def switch(self, container, representation): From bdc3a05c4d52a29c1aaff99d83c993be48c7563e Mon Sep 17 00:00:00 2001 From: Pype Club Date: Tue, 22 Mar 2022 16:38:46 +0100 Subject: [PATCH 048/291] OP-2766 - fix wrongly used functions --- openpype/hosts/photoshop/api/pipeline.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 2f4343753c..abc4e63bf6 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -179,10 +179,10 @@ def remove_instance(instance): stub.remove_instance(inst_id) if instance.get("members"): - item = stub.get_item(instance["members"][0]) + item = stub.get_layer(instance["members"][0]) if item: - stub.rename_item(item.id, - item.name.replace(stub.PUBLISH_ICON, '')) + stub.rename_layer(item.id, + item.name.replace(stub.PUBLISH_ICON, '')) def _get_stub(): From b8dd330be3f0de72ba1a28652dff2ae4702c3dc2 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Tue, 22 Mar 2022 16:40:28 +0100 Subject: [PATCH 049/291] OP-2766 - fix new creator for multiple instance's update --- .../hosts/photoshop/plugins/create/create_image.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index c24d8bde2f..bc0fa6a051 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -102,11 +102,11 @@ class ImageCreator(Creator): def update_instances(self, update_list): self.log.debug("update_list:: {}".format(update_list)) - created_inst, changes = update_list[0] - if created_inst.get("layer"): - created_inst.pop("layer") # not storing PSItem layer to metadata - api.stub().imprint(created_inst.get("instance_id"), - created_inst.data_to_store()) + for created_inst, _changes in update_list: + if created_inst.get("layer"): + created_inst.pop("layer") # not storing PSItem layer to metadata + api.stub().imprint(created_inst.get("instance_id"), + created_inst.data_to_store()) def remove_instances(self, instances): for instance in instances: From adc135cb4c1d09eb27d51dae067f054a93c74d77 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Tue, 22 Mar 2022 16:59:53 +0100 Subject: [PATCH 050/291] OP-2766 - added newPublishing flag to differentiate old from new --- openpype/plugins/publish/collect_from_create_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/publish/collect_from_create_context.py b/openpype/plugins/publish/collect_from_create_context.py index 16e3f669c3..09584ab37c 100644 --- a/openpype/plugins/publish/collect_from_create_context.py +++ b/openpype/plugins/publish/collect_from_create_context.py @@ -25,7 +25,7 @@ class CollectFromCreateContext(pyblish.api.ContextPlugin): # Update global data to context context.data.update(create_context.context_data_to_store()) - + context.data["newPublishing"] = True # Update context data for key in ("AVALON_PROJECT", "AVALON_ASSET", "AVALON_TASK"): value = create_context.dbcon.Session.get(key) From 96d88e592d56cb5193a13764aba9f5fcecff9616 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Tue, 22 Mar 2022 17:01:35 +0100 Subject: [PATCH 051/291] OP-2766 - renamed collector --- openpype/hosts/photoshop/plugins/publish/collect_instances.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index ee402dcabf..d506b9a5bf 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -13,7 +13,7 @@ class CollectInstances(pyblish.api.ContextPlugin): id (str): "pyblish.avalon.instance" """ - label = "Instances" + label = "Collect Instances" order = pyblish.api.CollectorOrder hosts = ["photoshop"] families_mapping = { From e86dc1acd77b841d36486a594862473e6aaf76a8 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Tue, 22 Mar 2022 19:57:02 +0100 Subject: [PATCH 052/291] OP-2766 - refactored new creator --- .../photoshop/plugins/create/create_image.py | 79 ++++++++----------- 1 file changed, 33 insertions(+), 46 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index bc0fa6a051..cd7e219bd0 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -3,8 +3,7 @@ from openpype.hosts.photoshop import api from openpype.pipeline import ( Creator, CreatedInstance, - lib, - CreatorError + lib ) @@ -30,65 +29,53 @@ class ImageCreator(Creator): ) self._add_instance_to_context(instance) - def create(self, subset_name, data, pre_create_data): - groups = [] - layers = [] - create_group = False + def create(self, subset_name_from_ui, data, pre_create_data): + groups_to_create = [] + top_layers_to_wrap = [] + create_empty_group = False stub = api.stub() # only after PS is up - multiple_instances = pre_create_data.get("create_multiple") - selection = stub.get_selected_layers() + top_level_selected_items = stub.get_selected_layers() if pre_create_data.get("use_selection"): - if len(selection) > 1: - if multiple_instances: - for item in selection: - if item.group: - groups.append(item) - else: - layers.append(item) + only_single_item_selected = len(top_level_selected_items) == 1 + for selected_item in top_level_selected_items: + if only_single_item_selected or pre_create_data.get("create_multiple"): + if selected_item.group: + groups_to_create.append(selected_item) + else: + top_layers_to_wrap.append(selected_item) else: - group = stub.group_selected_layers(subset_name) - groups.append(group) - elif len(selection) == 1: - # One selected item. Use group if its a LayerSet (group), else - # create a new group. - selected_item = selection[0] - if selected_item.group: - groups.append(selected_item) - else: - layers.append(selected_item) - elif len(selection) == 0: - # No selection creates an empty group. - create_group = True - else: - group = stub.create_group(subset_name) - groups.append(group) + group = stub.group_selected_layers(subset_name_from_ui) + groups_to_create.append(group) - if create_group: - group = stub.create_group(subset_name) - groups.append(group) + if not groups_to_create and not top_layers_to_wrap: + group = stub.create_group(subset_name_from_ui) + groups_to_create.append(group) - for layer in layers: + # wrap each top level layer into separate new group + for layer in top_layers_to_wrap: stub.select_layers([layer]) group = stub.group_selected_layers(layer.name) - groups.append(group) + groups_to_create.append(group) - for group in groups: - long_names = [] - group.name = self._clean_highlights(stub, group.name) + creating_multiple_groups = len(groups_to_create) > 1 + for group in groups_to_create: + subset_name = subset_name_from_ui # reset to name from creator UI + layer_names_in_hierarchy = [] + created_group_name = self._clean_highlights(stub, group.name) - if len(groups) > 1: + if creating_multiple_groups: + # concatenate with layer name to differentiate subsets subset_name += group.name.title().replace(" ", "") if group.long_name: for directory in group.long_name[::-1]: name = self._clean_highlights(stub, directory) - long_names.append(name) + layer_names_in_hierarchy.append(name) data.update({"subset": subset_name}) - data.update({"layer": group}) data.update({"members": [str(group.id)]}) - data.update({"long_name": "_".join(long_names)}) + data.update({"long_name": "_".join(layer_names_in_hierarchy)}) new_instance = CreatedInstance(self.family, subset_name, data, self) @@ -97,8 +84,8 @@ class ImageCreator(Creator): new_instance.data_to_store()) self._add_instance_to_context(new_instance) # reusing existing group, need to rename afterwards - if not create_group: - stub.rename_layer(group.id, stub.PUBLISH_ICON + group.name) + if not create_empty_group: + stub.rename_layer(group.id, stub.PUBLISH_ICON + created_group_name) def update_instances(self, update_list): self.log.debug("update_list:: {}".format(update_list)) @@ -120,7 +107,7 @@ class ImageCreator(Creator): def get_pre_create_attr_defs(self): output = [ - lib.BoolDef("use_selection", default=True, label="Use selection"), + lib.BoolDef("use_selection", default=True, label="Create only for selected"), lib.BoolDef("create_multiple", default=True, label="Create separate instance for each selected") From 9be8885bc3845d3fd5a4aed6b9558a3758e38a8b Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 23 Mar 2022 10:47:50 +0100 Subject: [PATCH 053/291] OP-2766 - added support for new publisher NP already collected instances, need to only add layer information --- .../plugins/publish/collect_instances.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index d506b9a5bf..1b30fb053a 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -1,3 +1,4 @@ +import pprint import pyblish.api from openpype.hosts.photoshop import api as photoshop @@ -21,9 +22,10 @@ class CollectInstances(pyblish.api.ContextPlugin): } def process(self, context): - if context.data.get("newPublishing"): - self.log.debug("Not applicable for New Publisher, skip") - return + instance_by_layer_id = {} + for instance in context: + if instance.data["family"] == "image" and instance.data.get("members"): + instance_by_layer_id[str(instance.data["members"][0])] = instance stub = photoshop.stub() layers = stub.get_layers() @@ -40,13 +42,10 @@ class CollectInstances(pyblish.api.ContextPlugin): if "container" in layer_data["id"]: continue - # child_layers = [*layer.Layers] - # self.log.debug("child_layers {}".format(child_layers)) - # if not child_layers: - # self.log.info("%s skipped, it was empty." % layer.Name) - # continue + instance = instance_by_layer_id.get(str(layer.id)) + if instance is None: + instance = context.create_instance(layer_data["subset"]) - instance = context.create_instance(layer_data["subset"]) instance.data["layer"] = layer instance.data.update(layer_data) instance.data["families"] = self.families_mapping[ @@ -58,7 +57,7 @@ class CollectInstances(pyblish.api.ContextPlugin): # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.info("instance: {} ".format(instance.data)) + self.log.info("instance: {} ".format(pprint.pformat(instance.data, indent=4))) if len(instance_names) != len(set(instance_names)): self.log.warning("Duplicate instances found. " + From 11a9ad18738ffa9ff036722f699d715663d3fb53 Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 23 Mar 2022 10:53:58 +0100 Subject: [PATCH 054/291] OP-2766 - refactor --- .../plugins/publish/collect_instances.py | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index 1b30fb053a..9449662067 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -7,8 +7,8 @@ from openpype.hosts.photoshop import api as photoshop class CollectInstances(pyblish.api.ContextPlugin): """Gather instances by LayerSet and file metadata - This collector takes into account assets that are associated with - an LayerSet and marked with a unique identifier; + Collects publishable instances from file metadata or enhance + already collected by creator (family == "image"). Identifier: id (str): "pyblish.avalon.instance" @@ -24,40 +24,44 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): instance_by_layer_id = {} for instance in context: - if instance.data["family"] == "image" and instance.data.get("members"): - instance_by_layer_id[str(instance.data["members"][0])] = instance + if ( + instance.data["family"] == "image" and + instance.data.get("members")): + layer_id = str(instance.data["members"][0]) + instance_by_layer_id[layer_id] = instance stub = photoshop.stub() - layers = stub.get_layers() + layer_items = stub.get_layers() layers_meta = stub.get_layers_metadata() instance_names = [] - for layer in layers: - layer_data = stub.read(layer, layers_meta) + for layer_item in layer_items: + layer_instance_data = stub.read(layer_item, layers_meta) # Skip layers without metadata. - if layer_data is None: + if layer_instance_data is None: continue # Skip containers. - if "container" in layer_data["id"]: + if "container" in layer_instance_data["id"]: continue - instance = instance_by_layer_id.get(str(layer.id)) + instance = instance_by_layer_id.get(str(layer_item.id)) if instance is None: - instance = context.create_instance(layer_data["subset"]) + instance = context.create_instance(layer_instance_data["subset"]) - instance.data["layer"] = layer - instance.data.update(layer_data) + instance.data["layer"] = layer_item + instance.data.update(layer_instance_data) instance.data["families"] = self.families_mapping[ - layer_data["family"] + layer_instance_data["family"] ] - instance.data["publish"] = layer.visible - instance_names.append(layer_data["subset"]) + instance.data["publish"] = layer_item.visible + instance_names.append(layer_instance_data["subset"]) # Produce diagnostic message for any graphical # user interface interested in visualising it. self.log.info("Found: \"%s\" " % instance.data["name"]) - self.log.info("instance: {} ".format(pprint.pformat(instance.data, indent=4))) + self.log.info("instance: {} ".format( + pprint.pformat(instance.data, indent=4))) if len(instance_names) != len(set(instance_names)): self.log.warning("Duplicate instances found. " + From 755a6dabfd1ba5d1bb80000ab69140d1a54d9c3d Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 23 Mar 2022 11:47:36 +0100 Subject: [PATCH 055/291] OP-2766 - added NP validators for subset names and uniqueness --- .../plugins/publish/help/validate_naming.xml | 21 +++++++++ .../publish/help/validate_unique_subsets.xml | 14 ++++++ .../plugins/publish/validate_naming.py | 47 +++++++++++-------- .../publish/validate_unique_subsets.py | 9 +++- 4 files changed, 71 insertions(+), 20 deletions(-) create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml create mode 100644 openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml new file mode 100644 index 0000000000..5a1e266748 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_naming.xml @@ -0,0 +1,21 @@ + + + +Subset name + +## Invalid subset or layer name + +Subset or layer name cannot contain specific characters (spaces etc) which could cause issue when subset name is used in a published file name. + {msg} + +### How to repair? + +You can fix this with "repair" button on the right. + + +### __Detailed Info__ (optional) + +Not all characters are available in a file names on all OS. Wrong characters could be configured in Settings. + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml new file mode 100644 index 0000000000..4b47973193 --- /dev/null +++ b/openpype/hosts/photoshop/plugins/publish/help/validate_unique_subsets.xml @@ -0,0 +1,14 @@ + + + +Subset not unique + +## Non unique subset name found + + Non unique subset names: '{non_unique}' +### How to repair? + +Remove offending instance, rename it to have unique name. Maybe layer name wasn't used for multiple instances? + + + \ No newline at end of file diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index b40e44d016..c0ca4cfb69 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -2,6 +2,7 @@ import re import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError from openpype.hosts.photoshop import api as photoshop @@ -22,32 +23,33 @@ class ValidateNamingRepair(pyblish.api.Action): failed.append(result["instance"]) invalid_chars, replace_char = plugin.get_replace_chars() - self.log.info("{} --- {}".format(invalid_chars, replace_char)) + self.log.debug("{} --- {}".format(invalid_chars, replace_char)) # Apply pyblish.logic to get the instances for the plug-in instances = pyblish.api.instances_by_plugin(failed, plugin) stub = photoshop.stub() for instance in instances: - self.log.info("validate_naming instance {}".format(instance)) - metadata = stub.read(instance[0]) - self.log.info("metadata instance {}".format(metadata)) - layer_name = None - if metadata.get("uuid"): - layer_data = stub.get_layer(metadata["uuid"]) - self.log.info("layer_data {}".format(layer_data)) - if layer_data: - layer_name = re.sub(invalid_chars, - replace_char, - layer_data.name) + self.log.debug("validate_naming instance {}".format(instance)) + current_layer_state = stub.get_layer(instance.data["layer"].id) + self.log.debug("current_layer_state instance {}".format(current_layer_state)) - stub.rename_layer(instance.data["uuid"], layer_name) + layer_meta = stub.read(current_layer_state) + instance_id = layer_meta.get("instance_id") or layer_meta.get("uuid") + if not instance_id: + self.log.warning("Unable to repair, cannot find layer") + continue + + layer_name = re.sub(invalid_chars, + replace_char, + current_layer_state.name) + + stub.rename_layer(current_layer_state.id, layer_name) subset_name = re.sub(invalid_chars, replace_char, - instance.data["name"]) + instance.data["subset"]) - instance[0].Name = layer_name or subset_name - metadata["subset"] = subset_name - stub.imprint(instance[0], metadata) + layer_meta["subset"] = subset_name + stub.imprint(instance_id, layer_meta) return True @@ -72,11 +74,18 @@ class ValidateNaming(pyblish.api.InstancePlugin): help_msg = ' Use Repair action (A) in Pyblish to fix it.' msg = "Name \"{}\" is not allowed.{}".format(instance.data["name"], help_msg) - assert not re.search(self.invalid_chars, instance.data["name"]), msg + + formatting_data = {"msg": msg} + if re.search(self.invalid_chars, instance.data["name"]): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) msg = "Subset \"{}\" is not allowed.{}".format(instance.data["subset"], help_msg) - assert not re.search(self.invalid_chars, instance.data["subset"]), msg + formatting_data = {"msg": msg} + if re.search(self.invalid_chars, instance.data["subset"]): + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) @classmethod def get_replace_chars(cls): diff --git a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py index 40abfb1bbd..01f2323157 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_unique_subsets.py @@ -1,6 +1,7 @@ import collections import pyblish.api import openpype.api +from openpype.pipeline import PublishXmlValidationError class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): @@ -27,4 +28,10 @@ class ValidateSubsetUniqueness(pyblish.api.ContextPlugin): if count > 1] msg = ("Instance subset names {} are not unique. ".format(non_unique) + "Remove duplicates via SubsetManager.") - assert not non_unique, msg + formatting_data = { + "non_unique": ",".join(non_unique) + } + + if non_unique: + raise PublishXmlValidationError(self, msg, + formatting_data=formatting_data) From 85b49da44e14ec82a93e43bd4f8f1571b403627a Mon Sep 17 00:00:00 2001 From: Pype Club Date: Wed, 23 Mar 2022 12:11:48 +0100 Subject: [PATCH 056/291] OP-2766 - skip non active instances --- .../plugins/publish/collect_instances.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index 9449662067..52a8310594 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -35,27 +35,30 @@ class CollectInstances(pyblish.api.ContextPlugin): layers_meta = stub.get_layers_metadata() instance_names = [] for layer_item in layer_items: - layer_instance_data = stub.read(layer_item, layers_meta) + layer_meta_data = stub.read(layer_item, layers_meta) # Skip layers without metadata. - if layer_instance_data is None: + if layer_meta_data is None: continue # Skip containers. - if "container" in layer_instance_data["id"]: + if "container" in layer_meta_data["id"]: + continue + + if not layer_meta_data.get("active", True): # active might not be in legacy meta continue instance = instance_by_layer_id.get(str(layer_item.id)) if instance is None: - instance = context.create_instance(layer_instance_data["subset"]) + instance = context.create_instance(layer_meta_data["subset"]) instance.data["layer"] = layer_item - instance.data.update(layer_instance_data) + instance.data.update(layer_meta_data) instance.data["families"] = self.families_mapping[ - layer_instance_data["family"] + layer_meta_data["family"] ] instance.data["publish"] = layer_item.visible - instance_names.append(layer_instance_data["subset"]) + instance_names.append(layer_meta_data["subset"]) # Produce diagnostic message for any graphical # user interface interested in visualising it. From d211471ea099f53d8349f33d7e20ad29da7f178c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 13:49:50 +0100 Subject: [PATCH 057/291] OP-2766 - Hound --- openpype/hosts/photoshop/api/pipeline.py | 4 ++-- .../photoshop/plugins/create/create_image.py | 15 +++++++++------ .../plugins/publish/collect_instances.py | 3 ++- .../photoshop/plugins/publish/validate_naming.py | 5 +++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 1b471ef1d3..db40e456db 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -151,7 +151,7 @@ def list_instances(): layers_meta = stub.get_layers_metadata() if layers_meta: for instance in layers_meta: - if instance.get("id") == "pyblish.avalon.instance": # TODO only this way? + if instance.get("id") == "pyblish.avalon.instance": instances.append(instance) return instances @@ -266,4 +266,4 @@ def get_context_title(): project_name = avalon.api.Session["AVALON_PROJECT"] asset_name = avalon.api.Session["AVALON_ASSET"] task_name = avalon.api.Session["AVALON_TASK"] - return "{}/{}/{}".format(project_name, asset_name, task_name) \ No newline at end of file + return "{}/{}/{}".format(project_name, asset_name, task_name) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index cd7e219bd0..e332cfd9c2 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -39,7 +39,9 @@ class ImageCreator(Creator): if pre_create_data.get("use_selection"): only_single_item_selected = len(top_level_selected_items) == 1 for selected_item in top_level_selected_items: - if only_single_item_selected or pre_create_data.get("create_multiple"): + if ( + only_single_item_selected or + pre_create_data.get("create_multiple")): if selected_item.group: groups_to_create.append(selected_item) else: @@ -85,13 +87,15 @@ class ImageCreator(Creator): self._add_instance_to_context(new_instance) # reusing existing group, need to rename afterwards if not create_empty_group: - stub.rename_layer(group.id, stub.PUBLISH_ICON + created_group_name) + stub.rename_layer(group.id, + stub.PUBLISH_ICON + created_group_name) def update_instances(self, update_list): self.log.debug("update_list:: {}".format(update_list)) for created_inst, _changes in update_list: if created_inst.get("layer"): - created_inst.pop("layer") # not storing PSItem layer to metadata + # not storing PSItem layer to metadata + created_inst.pop("layer") api.stub().imprint(created_inst.get("instance_id"), created_inst.data_to_store()) @@ -107,7 +111,8 @@ class ImageCreator(Creator): def get_pre_create_attr_defs(self): output = [ - lib.BoolDef("use_selection", default=True, label="Create only for selected"), + lib.BoolDef("use_selection", default=True, + label="Create only for selected"), lib.BoolDef("create_multiple", default=True, label="Create separate instance for each selected") @@ -138,5 +143,3 @@ class ImageCreator(Creator): def _clean_highlights(self, stub, item): return item.replace(stub.PUBLISH_ICON, '').replace(stub.LOADED_ICON, '') - - diff --git a/openpype/hosts/photoshop/plugins/publish/collect_instances.py b/openpype/hosts/photoshop/plugins/publish/collect_instances.py index 52a8310594..a7bb2d40c7 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_instances.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_instances.py @@ -45,7 +45,8 @@ class CollectInstances(pyblish.api.ContextPlugin): if "container" in layer_meta_data["id"]: continue - if not layer_meta_data.get("active", True): # active might not be in legacy meta + # active might not be in legacy meta + if not layer_meta_data.get("active", True): continue instance = instance_by_layer_id.get(str(layer_item.id)) diff --git a/openpype/hosts/photoshop/plugins/publish/validate_naming.py b/openpype/hosts/photoshop/plugins/publish/validate_naming.py index c0ca4cfb69..bcae24108c 100644 --- a/openpype/hosts/photoshop/plugins/publish/validate_naming.py +++ b/openpype/hosts/photoshop/plugins/publish/validate_naming.py @@ -31,10 +31,11 @@ class ValidateNamingRepair(pyblish.api.Action): for instance in instances: self.log.debug("validate_naming instance {}".format(instance)) current_layer_state = stub.get_layer(instance.data["layer"].id) - self.log.debug("current_layer_state instance {}".format(current_layer_state)) + self.log.debug("current_layer{}".format(current_layer_state)) layer_meta = stub.read(current_layer_state) - instance_id = layer_meta.get("instance_id") or layer_meta.get("uuid") + instance_id = (layer_meta.get("instance_id") or + layer_meta.get("uuid")) if not instance_id: self.log.warning("Unable to repair, cannot find layer") continue From 49d26ef9593271a6b36dfbdd353f7bed017478ad Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 14:11:39 +0100 Subject: [PATCH 058/291] OP-2766 - changed imports after refactor of attribute definitions --- openpype/hosts/photoshop/plugins/create/create_image.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index e332cfd9c2..12898bb7f4 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -1,9 +1,9 @@ from avalon import api as avalon_api from openpype.hosts.photoshop import api +from openpype.lib import BoolDef from openpype.pipeline import ( Creator, - CreatedInstance, - lib + CreatedInstance ) @@ -111,9 +111,9 @@ class ImageCreator(Creator): def get_pre_create_attr_defs(self): output = [ - lib.BoolDef("use_selection", default=True, + BoolDef("use_selection", default=True, label="Create only for selected"), - lib.BoolDef("create_multiple", + BoolDef("create_multiple", default=True, label="Create separate instance for each selected") ] From 7273fd44daa2ebb266c9f95f9beb0cbfad53258a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 14:14:35 +0100 Subject: [PATCH 059/291] OP-2765 - changed imports after refactor of attribute definitions --- .../hosts/aftereffects/plugins/create/create_render.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 1eff992fe0..826d438fa3 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -1,11 +1,11 @@ from avalon import api as avalon_api from openpype import resources +from openpype.lib import BoolDef, UISeparatorDef from openpype.hosts.aftereffects import api from openpype.pipeline import ( Creator, CreatedInstance, - lib, CreatorError ) @@ -86,13 +86,13 @@ class RenderCreator(Creator): ] def get_instance_attr_defs(self): - return [lib.BoolDef("farm", label="Render on farm")] + return [BoolDef("farm", label="Render on farm")] def get_pre_create_attr_defs(self): output = [ - lib.BoolDef("use_selection", default=True, label="Use selection"), - lib.UISeparatorDef(), - lib.BoolDef("farm", label="Render on farm") + BoolDef("use_selection", default=True, label="Use selection"), + UISeparatorDef(), + BoolDef("farm", label="Render on farm") ] return output From c829cc19ac675bbc9752980b805b69964cccb6b7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 14:15:16 +0100 Subject: [PATCH 060/291] OP-2765 - changed default variant --- openpype/hosts/aftereffects/plugins/create/create_render.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index 826d438fa3..c43ada84b5 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -79,11 +79,7 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) def get_default_variants(self): - return [ - "myVariant", - "variantTwo", - "different_variant" - ] + return ["Main"] def get_instance_attr_defs(self): return [BoolDef("farm", label="Render on farm")] From 1534c878d2e57dad50823d52d434feb2cecd3f10 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 14:33:49 +0100 Subject: [PATCH 061/291] OP-2766 - Hound --- openpype/hosts/photoshop/plugins/create/create_image.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/create_image.py b/openpype/hosts/photoshop/plugins/create/create_image.py index 12898bb7f4..c2fe8b6c78 100644 --- a/openpype/hosts/photoshop/plugins/create/create_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_image.py @@ -112,10 +112,10 @@ class ImageCreator(Creator): def get_pre_create_attr_defs(self): output = [ BoolDef("use_selection", default=True, - label="Create only for selected"), + label="Create only for selected"), BoolDef("create_multiple", - default=True, - label="Create separate instance for each selected") + default=True, + label="Create separate instance for each selected") ] return output From c7039e91f8665b1a3f47e317e5b807faee03783c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 15:20:43 +0100 Subject: [PATCH 062/291] OP-2766 - return back uuid for legacy creator --- openpype/hosts/photoshop/plugins/create/create_legacy_image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py index 6fa455fa03..9736471a26 100644 --- a/openpype/hosts/photoshop/plugins/create/create_legacy_image.py +++ b/openpype/hosts/photoshop/plugins/create/create_legacy_image.py @@ -91,6 +91,7 @@ class CreateImage(create.LegacyCreator): long_names.append(name) self.data.update({"subset": subset_name}) + self.data.update({"uuid": str(group.id)}) self.data.update({"members": [str(group.id)]}) self.data.update({"long_name": "_".join(long_names)}) stub.imprint(group, self.data) From 8964fdb754ff837028f032d6bafbdc3ef160aa31 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 17:11:25 +0100 Subject: [PATCH 063/291] OP-2766 - clean up import --- openpype/hosts/aftereffects/api/pipeline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/api/pipeline.py b/openpype/hosts/aftereffects/api/pipeline.py index 2a213e1b59..e14b8adc8c 100644 --- a/openpype/hosts/aftereffects/api/pipeline.py +++ b/openpype/hosts/aftereffects/api/pipeline.py @@ -11,12 +11,12 @@ from openpype import lib from openpype.api import Logger from openpype.pipeline import ( LegacyCreator, + BaseCreator, register_loader_plugin_path, deregister_loader_plugin_path, AVALON_CONTAINER_ID, ) import openpype.hosts.aftereffects -from openpype.pipeline import BaseCreator from openpype.lib import register_event_callback from .launch_logic import get_stub From 0858ee0ce8483c123a67525342fba6f782c15ae2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 17:15:46 +0100 Subject: [PATCH 064/291] OP-2765 - remove wrong logging function --- .../aftereffects/plugins/publish/collect_workfile.py | 4 ---- openpype/lib/__init__.py | 3 +-- openpype/lib/log.py | 11 ----------- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 67f037e6e6..f285ae49e4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -2,8 +2,6 @@ import os from avalon import api import pyblish.api -from openpype.lib import debug_log_instance - class CollectWorkfile(pyblish.api.ContextPlugin): """ Adds the AE render instances """ @@ -76,5 +74,3 @@ class CollectWorkfile(pyblish.api.ContextPlugin): } instance.data["representations"].append(representation) - - debug_log_instance(self.log, "Workfile instance", instance) diff --git a/openpype/lib/__init__.py b/openpype/lib/__init__.py index f02706e44f..e8b6d18f4e 100644 --- a/openpype/lib/__init__.py +++ b/openpype/lib/__init__.py @@ -63,7 +63,7 @@ from .execute import ( path_to_subprocess_arg, CREATE_NO_WINDOW ) -from .log import PypeLogger, timeit, debug_log_instance +from .log import PypeLogger, timeit from .path_templates import ( merge_dict, @@ -369,7 +369,6 @@ __all__ = [ "OpenPypeMongoConnection", "timeit", - "debug_log_instance", "is_overlapping_otio_ranges", "otio_range_with_handles", diff --git a/openpype/lib/log.py b/openpype/lib/log.py index 991dc3349a..c963807014 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -498,14 +498,3 @@ def timeit(method): print('%r %2.2f ms' % (method.__name__, (te - ts) * 1000)) return result return timed - - -def debug_log_instance(logger, msg, instance): - """Helper function to write instance.data as json""" - def _default_json(value): - return str(value) - - logger.debug(msg) - logger.debug( - json.dumps(instance.data, indent=4, default=_default_json) - ) From 91879de0ad4ed7859b4fa330bcc03685fd3d39ad Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 17:24:24 +0100 Subject: [PATCH 065/291] OP-2765 - revert of unwanted commit --- openpype/modules/log_viewer/log_view_module.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/modules/log_viewer/log_view_module.py b/openpype/modules/log_viewer/log_view_module.py index 5e141f6aa2..14be6b392e 100644 --- a/openpype/modules/log_viewer/log_view_module.py +++ b/openpype/modules/log_viewer/log_view_module.py @@ -8,7 +8,7 @@ class LogViewModule(OpenPypeModule, ITrayModule): def initialize(self, modules_settings): logging_settings = modules_settings[self.name] - self.enabled = False # logging_settings["enabled"] + self.enabled = logging_settings["enabled"] # Tray attributes self.window = None From bfbb2061bcbe900a05ac59ff1e4894f1ae4cefa5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 17:25:34 +0100 Subject: [PATCH 066/291] OP-2765 - revert of unwanted commit --- .../deadline/repository/custom/plugins/GlobalJobPreLoad.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py index ed932d35b9..eeb1f7744c 100644 --- a/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py +++ b/openpype/modules/deadline/repository/custom/plugins/GlobalJobPreLoad.py @@ -85,9 +85,7 @@ def inject_openpype_environment(deadlinePlugin): with open(export_url) as fp: contents = json.load(fp) for key, value in contents.items(): - print("key:: {}".format(key)) - if key != 'NUMBER_OF_PROCESSORS': - deadlinePlugin.SetProcessEnvironmentVariable(key, value) + deadlinePlugin.SetProcessEnvironmentVariable(key, value) print(">>> Removing temporary file") os.remove(export_url) From 16c919e93d0d65af801a10dff431058ec1da8203 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 17:26:40 +0100 Subject: [PATCH 067/291] OP-2765 - revert of unwanted commit --- openpype/hosts/harmony/plugins/publish/extract_render.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/harmony/plugins/publish/extract_render.py b/openpype/hosts/harmony/plugins/publish/extract_render.py index 49133d9608..2f8169248e 100644 --- a/openpype/hosts/harmony/plugins/publish/extract_render.py +++ b/openpype/hosts/harmony/plugins/publish/extract_render.py @@ -41,7 +41,6 @@ class ExtractRender(pyblish.api.InstancePlugin): func = """function %s(args) { node.setTextAttr(args[0], "DRAWING_NAME", 1, args[1]); - node.setTextAttr(args[0], 'MOVIE_PATH', 1, args[1]); } %s """ % (sig, sig) From 59f2adbf341334fcb0ef239ce082f2c50bfe6a43 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 17:27:48 +0100 Subject: [PATCH 068/291] OP-2765 - revert of unwanted commit --- openpype/lib/log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/lib/log.py b/openpype/lib/log.py index c963807014..f33385e0ba 100644 --- a/openpype/lib/log.py +++ b/openpype/lib/log.py @@ -23,7 +23,6 @@ import time import traceback import threading import copy -import json from . import Terminal from .mongo import ( From 881ec1579ec82460734e9bdf93e9d5c968525b1d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 23 Mar 2022 18:10:17 +0100 Subject: [PATCH 069/291] OP-2765 - fix exception if no file opened Should be refactored, merged 2 functions in code and extension. --- openpype/hosts/aftereffects/api/workio.py | 23 +++++++++++++--------- openpype/hosts/aftereffects/api/ws_stub.py | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/openpype/hosts/aftereffects/api/workio.py b/openpype/hosts/aftereffects/api/workio.py index 5a8f86ead5..d6c732285a 100644 --- a/openpype/hosts/aftereffects/api/workio.py +++ b/openpype/hosts/aftereffects/api/workio.py @@ -5,14 +5,6 @@ from openpype.pipeline import HOST_WORKFILE_EXTENSIONS from .launch_logic import get_stub -def _active_document(): - document_name = get_stub().get_active_document_name() - if not document_name: - return None - - return document_name - - def file_extensions(): return HOST_WORKFILE_EXTENSIONS["aftereffects"] @@ -39,7 +31,8 @@ def current_file(): full_name = get_stub().get_active_document_full_name() if full_name and full_name != "null": return os.path.normpath(full_name).replace("\\", "/") - except Exception: + except ValueError: + print("Nothing opened") pass return None @@ -47,3 +40,15 @@ def current_file(): def work_root(session): return os.path.normpath(session["AVALON_WORKDIR"]).replace("\\", "/") + + +def _active_document(): + # TODO merge with current_file - even in extension + document_name = None + try: + document_name = get_stub().get_active_document_name() + except ValueError: + print("Nothing opened") + pass + + return document_name diff --git a/openpype/hosts/aftereffects/api/ws_stub.py b/openpype/hosts/aftereffects/api/ws_stub.py index 1dfea697a1..9a6462fcd4 100644 --- a/openpype/hosts/aftereffects/api/ws_stub.py +++ b/openpype/hosts/aftereffects/api/ws_stub.py @@ -171,7 +171,7 @@ class AfterEffectsServerStub(): def get_active_document_full_name(self): """ - Returns just a name of active document via ws call + Returns absolute path of active document via ws call Returns(string): file name """ res = self.websocketserver.call(self.client.call( From 41d54727529b8f2b8a1580fd455616cbe5905da7 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 11:50:32 +0100 Subject: [PATCH 070/291] OP-2765 - implemented support for optional validation in new publisher --- .../plugins/publish/validate_scene_settings.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py index 0753e3c09a..14e224fdc2 100644 --- a/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py +++ b/openpype/hosts/aftereffects/plugins/publish/validate_scene_settings.py @@ -5,11 +5,15 @@ import re import pyblish.api -from openpype.pipeline import PublishXmlValidationError +from openpype.pipeline import ( + PublishXmlValidationError, + OptionalPyblishPluginMixin +) from openpype.hosts.aftereffects.api import get_asset_settings -class ValidateSceneSettings(pyblish.api.InstancePlugin): +class ValidateSceneSettings(OptionalPyblishPluginMixin, + pyblish.api.InstancePlugin): """ Ensures that Composition Settings (right mouse on comp) are same as in FTrack on task. @@ -59,6 +63,10 @@ class ValidateSceneSettings(pyblish.api.InstancePlugin): def process(self, instance): """Plugin entry point.""" + # Skip the instance if is not active by data on the instance + if not self.is_active(instance.data): + return + expected_settings = get_asset_settings() self.log.info("config from DB::{}".format(expected_settings)) From e5f605b1236893c9917a3ea2931f6f3e75650f27 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 11:51:57 +0100 Subject: [PATCH 071/291] OP-2765 - render.farm is in families not in family Better handling of potentially multiple instances. (Still requiring that there is only one publishable composition at the moment.) --- openpype/hosts/aftereffects/plugins/publish/collect_audio.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_audio.py b/openpype/hosts/aftereffects/plugins/publish/collect_audio.py index 80679725e6..8647ba498b 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_audio.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_audio.py @@ -17,12 +17,11 @@ class CollectAudio(pyblish.api.ContextPlugin): def process(self, context): for instance in context: - if instance.data["family"] == 'render.farm': + if 'render.farm' in instance.data.get("families", []): comp_id = instance.data["comp_id"] if not comp_id: self.log.debug("No comp_id filled in instance") - # @iLLiCiTiT QUESTION Should return or continue? - return + continue context.data["audioFile"] = os.path.normpath( get_stub().get_audio_url(comp_id) ).replace("\\", "/") From 71cd7a3fb0aad57e191fb0c520b09921d668d542 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 11:53:32 +0100 Subject: [PATCH 072/291] OP-2765 - added support for optional validations Asset and Task should be ALWAYS on instance, not on context. (Publishable instance might allow different context than "real context".) --- .../plugins/publish/collect_render.py | 18 +++++++++++------- openpype/lib/abstract_collect_render.py | 1 + 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index aa5bc58ac2..24d08b343e 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -22,6 +22,7 @@ class AERenderInstance(RenderInstance): projectEntity = attr.ib(default=None) stagingDir = attr.ib(default=None) app_version = attr.ib(default=None) + publish_attributes = attr.ib(default=None) class CollectAERender(abstract_collect_render.AbstractCollectRender): @@ -50,16 +51,21 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): current_file = context.data["currentFile"] version = context.data["version"] - asset_entity = context.data["assetEntity"] + project_entity = context.data["projectEntity"] compositions = CollectAERender.get_stub().get_items(True) compositions_by_id = {item.id: item for item in compositions} for inst in context: + if not inst.data["active"]: + continue + family = inst.data["family"] if family not in ["render", "renderLocal"]: # legacy continue + asset_entity = inst.data["assetEntity"] + item_id = inst.data["members"][0] work_area_info = CollectAERender.get_stub().get_work_area( @@ -78,9 +84,6 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): fps = work_area_info.frameRate # TODO add resolution when supported by extension - if not inst.data["active"]: - continue - subset_name = inst.data["subset"] instance = AERenderInstance( family=family, @@ -90,7 +93,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): source=current_file, label="{} - {}".format(subset_name, family), subset=subset_name, - asset=context.data["assetEntity"]["name"], + asset=inst.data["asset"], + task=inst.data["task"], attachTo=False, setMembers='', publish=True, @@ -112,8 +116,8 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): toBeRenderedOn='deadline', fps=fps, app_version=app_version, - anatomyData=deepcopy(context.data["anatomyData"]), - context=context + anatomyData=deepcopy(inst.data["anatomyData"]), + publish_attributes=inst.data.get("publish_attributes") ) comp = compositions_by_id.get(int(item_id)) diff --git a/openpype/lib/abstract_collect_render.py b/openpype/lib/abstract_collect_render.py index 029bd3ec39..cce161b51c 100644 --- a/openpype/lib/abstract_collect_render.py +++ b/openpype/lib/abstract_collect_render.py @@ -30,6 +30,7 @@ class RenderInstance(object): source = attr.ib() # path to source scene file label = attr.ib() # label to show in GUI subset = attr.ib() # subset name + task = attr.ib() # task name asset = attr.ib() # asset name (AVALON_ASSET) attachTo = attr.ib() # subset name to attach render to setMembers = attr.ib() # list of nodes/members producing render output From 0506c38e00008d26eb8ce7b8391b6f53844efed3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 11:54:13 +0100 Subject: [PATCH 073/291] OP-2765 - cleaned up workfile collector --- .../plugins/publish/collect_workfile.py | 66 +++++++++---------- .../plugins/publish/submit_publish_job.py | 12 +++- 2 files changed, 43 insertions(+), 35 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index f285ae49e4..ac552a6a5f 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -17,16 +17,37 @@ class CollectWorkfile(pyblish.api.ContextPlugin): existing_instance = instance break - task = api.Session["AVALON_TASK"] current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) + if existing_instance is None: # old publish + instance = self._get_new_instance(context, scene_file) + else: + instance = existing_instance + + # creating representation + representation = { + 'name': 'aep', + 'ext': 'aep', + 'files': scene_file, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation) + + def _get_new_instance(self, context, scene_file): + task = api.Session["AVALON_TASK"] version = context.data["version"] asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - shared_instance_data = { + # workfile instance + family = "workfile" + subset = family + task.capitalize() # TOOD use method + + instance_data = { "asset": asset_entity["name"], + "task": task, "frameStart": asset_entity["data"]["frameStart"], "frameEnd": asset_entity["data"]["frameEnd"], "handleStart": asset_entity["data"]["handleStart"], @@ -40,37 +61,16 @@ class CollectWorkfile(pyblish.api.ContextPlugin): project_entity["data"]["resolutionHeight"]), "pixelAspect": 1, "step": 1, - "version": version + "version": version, + "subset": subset, + "label": scene_file, + "family": family, + "families": [family], + "representations": list() } - # workfile instance - family = "workfile" - subset = family + task.capitalize() - if existing_instance is None: # old publish - # Create instance - instance = context.create_instance(subset) + # Create instance + instance = context.create_instance(subset) + instance.data.update(instance_data) - # creating instance data - instance.data.update({ - "subset": subset, - "label": scene_file, - "family": family, - "families": [family], - "representations": list() - }) - - # adding basic script data - instance.data.update(shared_instance_data) - else: - instance = existing_instance - instance.data["publish"] = True # for DL - - # creating representation - representation = { - 'name': 'aep', - 'ext': 'aep', - 'files': scene_file, - "stagingDir": staging_dir, - } - - instance.data["representations"].append(representation) + return instance \ No newline at end of file diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index fad4d14ea0..f624f40635 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -392,6 +392,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): list of instances """ + self.log.info("!!!!! _create_instances_for_aov") task = os.environ["AVALON_TASK"] subset = instance_data["subset"] cameras = instance_data.get("cameras", []) @@ -454,6 +455,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): break if instance_data.get("multipartExr"): + self.log.info("!!!!! _create_instances_for_aov add multipartExr") preview = True new_instance = copy(instance_data) @@ -519,9 +521,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ representations = [] collections, remainders = clique.assemble(exp_files) - + self.log.info("!!!!! _get_representations") # create representation for every collected sequento ce for collection in collections: + self.log.info("!!!!! collection") ext = collection.tail.lstrip(".") preview = False # if filtered aov name is found in filename, toggle it for @@ -533,6 +536,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): aov, list(collection)[0] ): + self.log.info("!!!!! add preview") preview = True break @@ -582,6 +586,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # add reminders as representations for remainder in remainders: + self.log.info("!!!!! remainder") ext = remainder.split(".")[-1] staging = os.path.dirname(remainder) @@ -602,7 +607,10 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "files": os.path.basename(remainder), "stagingDir": os.path.dirname(remainder), } - if "render" in instance.get("families"): + is_render_type = set(["render"]).\ + intersection(instance.get("families")) + if is_render_type: + self.log.info("!!!!! is_render_type") rep.update({ "fps": instance.get("fps"), "tags": ["review"] From 2c20f6832dadcc85c1ae4fda23d952b7ae7d2c92 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 11:59:53 +0100 Subject: [PATCH 074/291] Revert "OP-2765 - cleaned up workfile collector" This reverts commit 0506c38e --- .../plugins/publish/collect_workfile.py | 66 +++++++++---------- .../plugins/publish/submit_publish_job.py | 12 +--- 2 files changed, 35 insertions(+), 43 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index ac552a6a5f..f285ae49e4 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -17,37 +17,16 @@ class CollectWorkfile(pyblish.api.ContextPlugin): existing_instance = instance break + task = api.Session["AVALON_TASK"] current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) - if existing_instance is None: # old publish - instance = self._get_new_instance(context, scene_file) - else: - instance = existing_instance - - # creating representation - representation = { - 'name': 'aep', - 'ext': 'aep', - 'files': scene_file, - "stagingDir": staging_dir, - } - - instance.data["representations"].append(representation) - - def _get_new_instance(self, context, scene_file): - task = api.Session["AVALON_TASK"] version = context.data["version"] asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - # workfile instance - family = "workfile" - subset = family + task.capitalize() # TOOD use method - - instance_data = { + shared_instance_data = { "asset": asset_entity["name"], - "task": task, "frameStart": asset_entity["data"]["frameStart"], "frameEnd": asset_entity["data"]["frameEnd"], "handleStart": asset_entity["data"]["handleStart"], @@ -61,16 +40,37 @@ class CollectWorkfile(pyblish.api.ContextPlugin): project_entity["data"]["resolutionHeight"]), "pixelAspect": 1, "step": 1, - "version": version, - "subset": subset, - "label": scene_file, - "family": family, - "families": [family], - "representations": list() + "version": version } - # Create instance - instance = context.create_instance(subset) - instance.data.update(instance_data) + # workfile instance + family = "workfile" + subset = family + task.capitalize() + if existing_instance is None: # old publish + # Create instance + instance = context.create_instance(subset) - return instance \ No newline at end of file + # creating instance data + instance.data.update({ + "subset": subset, + "label": scene_file, + "family": family, + "families": [family], + "representations": list() + }) + + # adding basic script data + instance.data.update(shared_instance_data) + else: + instance = existing_instance + instance.data["publish"] = True # for DL + + # creating representation + representation = { + 'name': 'aep', + 'ext': 'aep', + 'files': scene_file, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation) diff --git a/openpype/modules/deadline/plugins/publish/submit_publish_job.py b/openpype/modules/deadline/plugins/publish/submit_publish_job.py index f624f40635..fad4d14ea0 100644 --- a/openpype/modules/deadline/plugins/publish/submit_publish_job.py +++ b/openpype/modules/deadline/plugins/publish/submit_publish_job.py @@ -392,7 +392,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): list of instances """ - self.log.info("!!!!! _create_instances_for_aov") task = os.environ["AVALON_TASK"] subset = instance_data["subset"] cameras = instance_data.get("cameras", []) @@ -455,7 +454,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): break if instance_data.get("multipartExr"): - self.log.info("!!!!! _create_instances_for_aov add multipartExr") preview = True new_instance = copy(instance_data) @@ -521,10 +519,9 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): """ representations = [] collections, remainders = clique.assemble(exp_files) - self.log.info("!!!!! _get_representations") + # create representation for every collected sequento ce for collection in collections: - self.log.info("!!!!! collection") ext = collection.tail.lstrip(".") preview = False # if filtered aov name is found in filename, toggle it for @@ -536,7 +533,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): aov, list(collection)[0] ): - self.log.info("!!!!! add preview") preview = True break @@ -586,7 +582,6 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): # add reminders as representations for remainder in remainders: - self.log.info("!!!!! remainder") ext = remainder.split(".")[-1] staging = os.path.dirname(remainder) @@ -607,10 +602,7 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): "files": os.path.basename(remainder), "stagingDir": os.path.dirname(remainder), } - is_render_type = set(["render"]).\ - intersection(instance.get("families")) - if is_render_type: - self.log.info("!!!!! is_render_type") + if "render" in instance.get("families"): rep.update({ "fps": instance.get("fps"), "tags": ["review"] From 349827b3a20a718130c214057081f0fdcaa9e41f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 12:00:37 +0100 Subject: [PATCH 075/291] OP-2765 - cleaned up workfile collector --- .../plugins/publish/collect_workfile.py | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index f285ae49e4..93c7a448c6 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -17,16 +17,37 @@ class CollectWorkfile(pyblish.api.ContextPlugin): existing_instance = instance break - task = api.Session["AVALON_TASK"] current_file = context.data["currentFile"] staging_dir = os.path.dirname(current_file) scene_file = os.path.basename(current_file) + if existing_instance is None: # old publish + instance = self._get_new_instance(context, scene_file) + else: + instance = existing_instance + + # creating representation + representation = { + 'name': 'aep', + 'ext': 'aep', + 'files': scene_file, + "stagingDir": staging_dir, + } + + instance.data["representations"].append(representation) + + def _get_new_instance(self, context, scene_file): + task = api.Session["AVALON_TASK"] version = context.data["version"] asset_entity = context.data["assetEntity"] project_entity = context.data["projectEntity"] - shared_instance_data = { + # workfile instance + family = "workfile" + subset = family + task.capitalize() # TOOD use method + + instance_data = { "asset": asset_entity["name"], + "task": task, "frameStart": asset_entity["data"]["frameStart"], "frameEnd": asset_entity["data"]["frameEnd"], "handleStart": asset_entity["data"]["handleStart"], @@ -40,37 +61,16 @@ class CollectWorkfile(pyblish.api.ContextPlugin): project_entity["data"]["resolutionHeight"]), "pixelAspect": 1, "step": 1, - "version": version + "version": version, + "subset": subset, + "label": scene_file, + "family": family, + "families": [family], + "representations": list() } - # workfile instance - family = "workfile" - subset = family + task.capitalize() - if existing_instance is None: # old publish - # Create instance - instance = context.create_instance(subset) + # Create instance + instance = context.create_instance(subset) + instance.data.update(instance_data) - # creating instance data - instance.data.update({ - "subset": subset, - "label": scene_file, - "family": family, - "families": [family], - "representations": list() - }) - - # adding basic script data - instance.data.update(shared_instance_data) - else: - instance = existing_instance - instance.data["publish"] = True # for DL - - # creating representation - representation = { - 'name': 'aep', - 'ext': 'aep', - 'files': scene_file, - "stagingDir": staging_dir, - } - - instance.data["representations"].append(representation) + return instance From 8b424f0b013b07c66a17e33d71aee2737c4effb4 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 14:58:44 +0100 Subject: [PATCH 076/291] OP-2764 - fixed missed keys for old publishing in AE --- .../hosts/aftereffects/plugins/publish/collect_render.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_render.py b/openpype/hosts/aftereffects/plugins/publish/collect_render.py index 24d08b343e..d64e7abc5f 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_render.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_render.py @@ -57,7 +57,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): compositions = CollectAERender.get_stub().get_items(True) compositions_by_id = {item.id: item for item in compositions} for inst in context: - if not inst.data["active"]: + if not inst.data.get("active", True): continue family = inst.data["family"] @@ -84,6 +84,9 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): fps = work_area_info.frameRate # TODO add resolution when supported by extension + task_name = (inst.data.get("task") or + list(asset_entity["data"]["tasks"].keys())[0]) # lega + subset_name = inst.data["subset"] instance = AERenderInstance( family=family, @@ -94,7 +97,7 @@ class CollectAERender(abstract_collect_render.AbstractCollectRender): label="{} - {}".format(subset_name, family), subset=subset_name, asset=inst.data["asset"], - task=inst.data["task"], + task=task_name, attachTo=False, setMembers='', publish=True, From 4dcf12ee4c7c77af12c1620c756f4453b31c40c6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 25 Mar 2022 15:30:28 +0100 Subject: [PATCH 077/291] OP-2764 - scene should be always saved --- .../aftereffects/plugins/publish/extract_save_scene.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py b/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py index e20598b311..eb2977309f 100644 --- a/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py +++ b/openpype/hosts/aftereffects/plugins/publish/extract_save_scene.py @@ -1,15 +1,16 @@ +import pyblish.api + import openpype.api from openpype.hosts.aftereffects.api import get_stub -class ExtractSaveScene(openpype.api.Extractor): +class ExtractSaveScene(pyblish.api.ContextPlugin): """Save scene before extraction.""" order = openpype.api.Extractor.order - 0.48 label = "Extract Save Scene" hosts = ["aftereffects"] - families = ["workfile"] - def process(self, instance): + def process(self, context): stub = get_stub() stub.save() From ea8b3b79b1c3426194a49db7ac5c6d909a0c1d38 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 13:34:25 +0200 Subject: [PATCH 078/291] OP-2951 - added force_only_broken argument to sync methods Cleaned up representation in sync methods --- .../modules/sync_server/sync_server_module.py | 46 +++++++++++-------- openpype/modules/sync_server/utils.py | 5 ++ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index caf58503f1..9895a6d430 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -23,7 +23,7 @@ from openpype.settings.lib import ( from .providers.local_drive import LocalDriveHandler from .providers import lib -from .utils import time_function, SyncStatus +from .utils import time_function, SyncStatus, SiteAlreadyPresentError log = PypeLogger().get_logger("SyncServer") @@ -129,7 +129,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ Start of Public API """ def add_site(self, collection, representation_id, site_name=None, - force=False): + force=False, force_only_broken=False): """ Adds new site to representation to be synced. @@ -143,6 +143,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id (string): MongoDB _id value site_name (string): name of configured and active site force (bool): reset site if exists + force_only_broken (bool): reset only if "error" present Returns: throws ValueError if any issue @@ -155,7 +156,9 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.reset_site_on_representation(collection, representation_id, - site_name=site_name, force=force) + site_name=site_name, + force=force, + force_only_broken=force_only_broken) # public facing API def remove_site(self, collection, representation_id, site_name, @@ -281,7 +284,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): os.path.getmtime(local_file_path)) elem = {"name": site_name, "created_dt": created_dt} - self._add_site(collection, query, [repre], elem, + self._add_site(collection, query, repre, elem, site_name=site_name, file_id=repre_file["_id"]) sites_added += 1 @@ -819,7 +822,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self.log.debug("Adding alternate {} to {}".format( alt_site, representation["_id"])) self._add_site(collection, query, - [representation], elem, + representation, elem, alt_site, file_id=file_id, force=True) """ End of Public API """ @@ -1394,7 +1397,8 @@ class SyncServerModule(OpenPypeModule, ITrayModule): def reset_site_on_representation(self, collection, representation_id, side=None, file_id=None, site_name=None, - remove=False, pause=None, force=False): + remove=False, pause=None, force=False, + force_only_broken=False): """ Reset information about synchronization for particular 'file_id' and provider. @@ -1417,6 +1421,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): remove (bool): if True remove site altogether pause (bool or None): if True - pause, False - unpause force (bool): hard reset - currently only for add_site + force_only_broken(bool): reset site only if there is "error" field Returns: throws ValueError @@ -1425,7 +1430,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): "_id": ObjectId(representation_id) } - representation = list(self.connection.database[collection].find(query)) + representation = self.connection.database[collection].find_one(query) if not representation: raise ValueError("Representation {} not found in {}". format(representation_id, collection)) @@ -1456,7 +1461,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation, site_name, pause) else: # add new site to all files for representation self._add_site(collection, query, representation, elem, site_name, - force) + force=force, force_only_broken=force_only_broken) def _update_site(self, collection, query, update, arr_filter): """ @@ -1511,7 +1516,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): Throws ValueError if 'site_name' not found on 'representation' """ found = False - for repre_file in representation.pop().get("files"): + for repre_file in representation.get("files"): for site in repre_file.get("sites"): if site.get("name") == site_name: found = True @@ -1537,7 +1542,7 @@ class SyncServerModule(OpenPypeModule, ITrayModule): """ found = False site = None - for repre_file in representation.pop().get("files"): + for repre_file in representation.get("files"): for site in repre_file.get("sites"): if site["name"] == site_name: found = True @@ -1564,34 +1569,39 @@ class SyncServerModule(OpenPypeModule, ITrayModule): self._update_site(collection, query, update, arr_filter) def _add_site(self, collection, query, representation, elem, site_name, - force=False, file_id=None): + force=False, file_id=None, force_only_broken=False): """ Adds 'site_name' to 'representation' on 'collection' Args: - representation (list of 1 dict) + representation (dict) file_id (ObjectId) Use 'force' to remove existing or raises ValueError """ - reseted_existing = False - for repre_file in representation.pop().get("files"): + reset_existing = False + files = representation.get("files", []) + if not files: + log.debug("No files for {}".format(representation["_id"])) + return + + for repre_file in files: if file_id and file_id != repre_file["_id"]: continue for site in repre_file.get("sites"): if site["name"] == site_name: - if force: + if force or (force_only_broken and site.get("error")): self._reset_site_for_file(collection, query, elem, repre_file["_id"], site_name) - reseted_existing = True + reset_existing = True else: msg = "Site {} already present".format(site_name) log.info(msg) - raise ValueError(msg) + raise SiteAlreadyPresentError(msg) - if reseted_existing: + if reset_existing: return if not file_id: diff --git a/openpype/modules/sync_server/utils.py b/openpype/modules/sync_server/utils.py index 85e4e03f77..03f362202f 100644 --- a/openpype/modules/sync_server/utils.py +++ b/openpype/modules/sync_server/utils.py @@ -8,6 +8,11 @@ class ResumableError(Exception): pass +class SiteAlreadyPresentError(Exception): + """Representation has already site skeleton present.""" + pass + + class SyncStatus: DO_NOTHING = 0 DO_UPLOAD = 1 From d340d05bf01a5f8beda6cdae1736cb59219c4a07 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 13:37:12 +0200 Subject: [PATCH 079/291] OP-2951 - implemented synching referenced files in workfile When workfile is synched, it checks for referenced files (added by Loader) and tries to sync them too. --- openpype/plugins/load/add_site.py | 72 ++++++++++++++++++++++++++----- 1 file changed, 61 insertions(+), 11 deletions(-) diff --git a/openpype/plugins/load/add_site.py b/openpype/plugins/load/add_site.py index 95001691e2..0ddce6e160 100644 --- a/openpype/plugins/load/add_site.py +++ b/openpype/plugins/load/add_site.py @@ -1,9 +1,19 @@ from openpype.modules import ModulesManager from openpype.pipeline import load +:from openpype.lib.avalon_context import get_linked_ids_for_representations +from openpype.modules.sync_server.utils import SiteAlreadyPresentError class AddSyncSite(load.LoaderPlugin): - """Add sync site to representation""" + """Add sync site to representation + + If family of synced representation is 'workfile', it looks for all + representations which are referenced (loaded) in workfile with content of + 'inputLinks'. + It doesn't do any checks for site, most common use case is when artist is + downloading workfile to his local site, but it might be helpful when + artist is re-uploading broken representation on remote site also. + """ representations = ["*"] families = ["*"] @@ -12,21 +22,61 @@ class AddSyncSite(load.LoaderPlugin): icon = "download" color = "#999999" + _sync_server = None + + @property + def sync_server(self): + if not self._sync_server: + manager = ModulesManager() + self._sync_server = manager.modules_by_name["sync_server"] + + return self._sync_server + def load(self, context, name=None, namespace=None, data=None): self.log.info("Adding {} to representation: {}".format( data["site_name"], data["_id"])) - self.add_site_to_representation(data["project_name"], - data["_id"], - data["site_name"]) + family = context["representation"]["context"]["family"] + project_name = data["project_name"] + repre_id = data["_id"] + + add_ids = [repre_id] + if family == "workfile": + links = get_linked_ids_for_representations(project_name, + add_ids, + link_type="reference") + add_ids.extend(links) + + add_ids = set(add_ids) + self.log.info("Add to repre_ids {}".format(add_ids)) + is_main = True + for add_repre_id in add_ids: + self.add_site_to_representation(project_name, + add_repre_id, + data["site_name"], + is_main) + is_main = False + self.log.debug("Site added.") - @staticmethod - def add_site_to_representation(project_name, representation_id, site_name): - """Adds new site to representation_id, resets if exists""" - manager = ModulesManager() - sync_server = manager.modules_by_name["sync_server"] - sync_server.add_site(project_name, representation_id, site_name, - force=True) + def add_site_to_representation(self, project_name, representation_id, + site_name, is_main): + """Adds new site to representation_id, resets if exists + + Args: + project_name (str) + representation_id (ObjectId): + site_name (str) + is_main (bool): true for really downloaded, false for references, + force redownload main file always, for references only if + broken + """ + try: + self.sync_server.add_site(project_name, representation_id, + site_name, + force=is_main, + force_only_broken=not is_main) + except SiteAlreadyPresentError: + self.log.debug("Site present", exc_info=True) def filepath_from_context(self, context): """No real file loading""" From a197334a251404d06f89ec3de6940db68c4b1dde Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 13:57:21 +0200 Subject: [PATCH 080/291] OP-2951 - added function to collect referenced representation ids --- openpype/lib/avalon_context.py | 120 +++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index b4e6abb72d..e8a365ec39 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1971,3 +1971,123 @@ def get_last_workfile( return os.path.normpath(os.path.join(workdir, filename)) return filename + + +@with_avalon +def get_linked_ids_for_representations(project, repre_ids, dbcon=None, + link_type=None, max_depth=0): + """Returns list of linked ids of particular type (if provided). + + Goes from representations to version, back to representations + Args: + project (str) + repre_ids (list) or (ObjectId) + dbcon (avalon.mongodb.AvalonMongoDB, optional): Avalon Mongo connection + with Session. + link_type (str): ['reference', '..] + max_depth (int): limit how many levels of recursion + Returns: + (list) of ObjectId - linked representations + """ + if not dbcon: + log.debug("Using `avalon.io` for query.") + dbcon = avalon.io + # Make sure is installed + dbcon.install() + + if dbcon.Session["AVALON_PROJECT"] != project: + dbcon.Session["AVALON_PROJECT"] = project + + if not isinstance(repre_ids, list): + repre_ids = [repre_ids] + + versions = avalon.io.find( + { + "_id": {"$in": repre_ids}, + "type": "representation" + }, + projection={"parent": True} + ) + version_ids = [version["parent"] for version in versions] + + graph_lookup = { + "from": project, + "startWith": "$data.inputLinks.id", + "connectFromField": "data.inputLinks.id", + "connectToField": "_id", + "as": "outputs_recursive", + "depthField": "depth" + } + if max_depth != 0: + # We offset by -1 since 0 basically means no recursion + # but the recursion only happens after the initial lookup + # for outputs. + graph_lookup["maxDepth"] = max_depth - 1 + + match = { + "_id": {"$in": version_ids}, + "type": "version" + } + + pipeline_ = [ + # Match + {"$match": match}, + # Recursive graph lookup for inputs + {"$graphLookup": graph_lookup} + ] + + result = dbcon.aggregate(pipeline_) + referenced_version_ids = _process_referenced_pipeline_result(result, + link_type) + + representations = avalon.io.find( + { + "parent": {"$in": list(referenced_version_ids)}, + "type": "representation" + }, + projection={"_id": True} + ) + ref_ids = {representation["_id"] for representation in representations} + return list(ref_ids) + + +def _process_referenced_pipeline_result(result, link_type): + """Filters result from pipeline for particular link_type. + + Pipeline cannot use link_type directly in a query. + Returns: + (list) + """ + referenced_version_ids = set() + correctly_linked_ids = set() + for item in result: + correctly_linked_ids = _filter_input_links(item["data"]["inputLinks"], + link_type, + correctly_linked_ids) + + # outputs_recursive in random order, sort by _id + outputs_recursive = sorted(item.get("outputs_recursive", []), + key=lambda d: d["_id"]) + # go from oldest to newest + # only older _id can reference another newer _id + for output in outputs_recursive[::-1]: + if output["_id"] not in correctly_linked_ids: # leaf + continue + + correctly_linked_ids = _filter_input_links( + output["data"].get("inputLinks", []), + link_type, + correctly_linked_ids) + + referenced_version_ids.add(output["_id"]) + + return referenced_version_ids + + +def _filter_input_links(input_links, link_type, correctly_linked_ids): + for input_link in input_links: + if not link_type or input_link["type"] == link_type: + correctly_linked_ids.add(input_link.get("id") or + input_link.get("_id")) # legacy + + return correctly_linked_ids From a0a2e2678e55f449201981b419d9a6a13f8b4a49 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 14:07:57 +0200 Subject: [PATCH 081/291] OP-2951 - fixed typo --- openpype/plugins/load/add_site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/load/add_site.py b/openpype/plugins/load/add_site.py index 0ddce6e160..59720eb5b6 100644 --- a/openpype/plugins/load/add_site.py +++ b/openpype/plugins/load/add_site.py @@ -1,6 +1,6 @@ from openpype.modules import ModulesManager from openpype.pipeline import load -:from openpype.lib.avalon_context import get_linked_ids_for_representations +from openpype.lib.avalon_context import get_linked_ids_for_representations from openpype.modules.sync_server.utils import SiteAlreadyPresentError From af092348e50e1bda0ac6b3a13a58f1908cf5b939 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 16:32:32 +0200 Subject: [PATCH 082/291] OP-2766 - Fix creation of subset names in PS review and workfile --- .../hosts/photoshop/plugins/publish/collect_review.py | 10 +++++++++- .../photoshop/plugins/publish/collect_workfile.py | 10 +++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 4b6f855a6a..dafeb95d0e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -10,6 +10,8 @@ import os import pyblish.api +from openpype.lib import get_subset_name + class CollectReview(pyblish.api.ContextPlugin): """Gather the active document as review instance. @@ -25,7 +27,13 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" task = os.getenv("AVALON_TASK", None) - subset = family + task.capitalize() + subset = get_subset_name( + family, + "", + task, + context.data["assetEntity"]["_id"], + host_name="photoshop" + ) instance = context.create_instance(subset) instance.data.update({ diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index bdbd379a33..1a826c3f2a 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,6 +1,8 @@ import os import pyblish.api +from openpype.lib import get_subset_name + class CollectWorkfile(pyblish.api.ContextPlugin): """Collect current script for publish.""" @@ -19,7 +21,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): family = "workfile" task = os.getenv("AVALON_TASK", None) - subset = family + task.capitalize() + subset = get_subset_name( + family, + "", + task, + context.data["assetEntity"]["_id"], + host_name="photoshop" + ) file_path = context.data["currentFile"] staging_dir = os.path.dirname(file_path) From 0f08f3e31df5a6ec54c025776d490343a587ab5b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 29 Mar 2022 17:25:19 +0200 Subject: [PATCH 083/291] OP-2766 - Fix pulling task and project from context --- openpype/hosts/photoshop/plugins/publish/collect_review.py | 5 +++-- openpype/hosts/photoshop/plugins/publish/collect_workfile.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index dafeb95d0e..09fed2df78 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -23,15 +23,16 @@ class CollectReview(pyblish.api.ContextPlugin): label = "Collect Review" order = pyblish.api.CollectorOrder hosts = ["photoshop"] + order = pyblish.api.CollectorOrder + 0.1 def process(self, context): family = "review" - task = os.getenv("AVALON_TASK", None) subset = get_subset_name( family, "", - task, + context.data["anatomyData"]["task"]["name"], context.data["assetEntity"]["_id"], + context.data["anatomyData"]["project"]["name"], host_name="photoshop" ) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 1a826c3f2a..71022a86fd 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -20,12 +20,12 @@ class CollectWorkfile(pyblish.api.ContextPlugin): break family = "workfile" - task = os.getenv("AVALON_TASK", None) subset = get_subset_name( family, "", - task, + context.data["anatomyData"]["task"]["name"], context.data["assetEntity"]["_id"], + context.data["anatomyData"]["project"]["name"], host_name="photoshop" ) From ee885051d915a20fe5e004a27c40a246f1e156de Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 30 Mar 2022 12:35:27 +0200 Subject: [PATCH 084/291] Added compute_resource_sync_sites to sync_server_module This method will be used in integrate_new to logically separate Site Sync parts. --- .../modules/sync_server/sync_server_module.py | 107 +++++++++++++++++- 1 file changed, 106 insertions(+), 1 deletion(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index caf58503f1..7126c17e17 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -157,7 +157,6 @@ class SyncServerModule(OpenPypeModule, ITrayModule): representation_id, site_name=site_name, force=force) - # public facing API def remove_site(self, collection, representation_id, site_name, remove_local_files=False): """ @@ -184,6 +183,112 @@ class SyncServerModule(OpenPypeModule, ITrayModule): if remove_local_files: self._remove_local_file(collection, representation_id, site_name) + def compute_resource_sync_sites(self, project_name): + """Get available resource sync sites state for publish process. + + Returns dict with prepared state of sync sites for 'project_name'. + It checks if Site Sync is enabled, handles alternative sites. + Publish process stores this dictionary as a part of representation + document in DB. + + Example: + [ + { + 'name': '42abbc09-d62a-44a4-815c-a12cd679d2d7', + 'created_dt': datetime.datetime(2022, 3, 30, 12, 16, 9, 778637) + }, + {'name': 'studio'}, + {'name': 'SFTP'} + ] -- representation is published locally, artist or Settings have set + remote site as 'studio'. 'SFTP' is alternate site to 'studio'. Eg. + whenever file is on 'studio', it is also on 'SFTP'. + """ + + def create_metadata(name, created=True): + """Create sync site metadata for site with `name`""" + metadata = {"name": name} + if created: + metadata["created_dt"] = datetime.now() + return metadata + + if ( + not self.sync_system_settings["enabled"] or + not self.sync_project_settings[project_name]["enabled"]): + return [create_metadata(self.DEFAULT_SITE)] + + local_site = self.get_active_site(project_name) + remote_site = self.get_remote_site(project_name) + + # Attached sites metadata by site name + # That is the local site, remote site, the always accesible sites + # and their alternate sites (alias of sites with different protocol) + attached_sites = dict() + attached_sites[local_site] = create_metadata(local_site) + + if remote_site and remote_site not in attached_sites: + attached_sites[remote_site] = create_metadata(remote_site, + created=False) + + # add skeleton for sites where it should be always synced to + # usually it would be a backup site which is handled by separate + # background process + for site in self._get_always_accessible_sites(project_name): + if site not in attached_sites: + attached_sites[site] = create_metadata(site, created=False) + + attached_sites = self._add_alternative_sites(attached_sites) + + return list(attached_sites.values()) + + def _get_always_accessible_sites(self, project_name): + """Sites that synced to as a part of background process. + + Artist machine doesn't handle those, explicit Tray with that site name + as a local id must be running. + Example is dropbox site serving as a backup solution + """ + always_accessible_sites = ( + self.get_sync_project_setting(project_name)["config"]. + get("always_accessible_on", []) + ) + return [site.strip() for site in always_accessible_sites] + + def _add_alternative_sites(self, attached_sites): + """Add skeleton document for alternative sites + + Each new configured site in System Setting could serve as a alternative + site, it's a kind of alias. It means that files on 'a site' are + physically accessible also on 'a alternative' site. + Example is sftp site serving studio files via sftp protocol, physically + file is only in studio, sftp server has this location mounted. + """ + additional_sites = self.sync_system_settings.get("sites", {}) + + for site_name, site_info in additional_sites.items(): + # Get alternate sites (stripped names) for this site name + alt_sites = site_info.get("alternative_sites", []) + alt_sites = [site.strip() for site in alt_sites] + alt_sites = set(alt_sites) + + # If no alternative sites we don't need to add + if not alt_sites: + continue + + # Take a copy of data of the first alternate site that is already + # defined as an attached site to match the same state. + match_meta = next((attached_sites[site] for site in alt_sites + if site in attached_sites), None) + if not match_meta: + continue + + alt_site_meta = copy.deepcopy(match_meta) + alt_site_meta["name"] = site_name + + # Note: We change mutable `attached_site` dict in-place + attached_sites[site_name] = alt_site_meta + + return attached_sites + def clear_project(self, collection, site_name): """ Clear 'collection' of 'site_name' and its local files From 9e4e6d4b85a1273d1eeab0f473c88cb8b7f62f30 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 30 Mar 2022 13:30:53 +0200 Subject: [PATCH 085/291] OP-2766 Switched subset function according to review comments --- .../hosts/photoshop/plugins/publish/collect_review.py | 8 ++++---- .../hosts/photoshop/plugins/publish/collect_workfile.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_review.py b/openpype/hosts/photoshop/plugins/publish/collect_review.py index 09fed2df78..d825950b9e 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_review.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_review.py @@ -10,7 +10,7 @@ import os import pyblish.api -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectReview(pyblish.api.ContextPlugin): @@ -27,13 +27,13 @@ class CollectReview(pyblish.api.ContextPlugin): def process(self, context): family = "review" - subset = get_subset_name( + subset = get_subset_name_with_asset_doc( family, "", context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"]["_id"], + context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name="photoshop" + host_name=context.data["hostName"] ) instance = context.create_instance(subset) diff --git a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py index 71022a86fd..e4f0a07b34 100644 --- a/openpype/hosts/photoshop/plugins/publish/collect_workfile.py +++ b/openpype/hosts/photoshop/plugins/publish/collect_workfile.py @@ -1,7 +1,7 @@ import os import pyblish.api -from openpype.lib import get_subset_name +from openpype.lib import get_subset_name_with_asset_doc class CollectWorkfile(pyblish.api.ContextPlugin): @@ -20,13 +20,13 @@ class CollectWorkfile(pyblish.api.ContextPlugin): break family = "workfile" - subset = get_subset_name( + subset = get_subset_name_with_asset_doc( family, "", context.data["anatomyData"]["task"]["name"], - context.data["assetEntity"]["_id"], + context.data["assetEntity"], context.data["anatomyData"]["project"]["name"], - host_name="photoshop" + host_name=context.data["hostName"] ) file_path = context.data["currentFile"] From f6fb60bb49bed7a0c26825ef85b5d0f65c4aa6bb Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 11:53:58 +0200 Subject: [PATCH 086/291] Update openpype/plugins/load/add_site.py Co-authored-by: Roy Nieterau --- openpype/plugins/load/add_site.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/openpype/plugins/load/add_site.py b/openpype/plugins/load/add_site.py index 59720eb5b6..e26ef586e0 100644 --- a/openpype/plugins/load/add_site.py +++ b/openpype/plugins/load/add_site.py @@ -38,23 +38,20 @@ class AddSyncSite(load.LoaderPlugin): family = context["representation"]["context"]["family"] project_name = data["project_name"] repre_id = data["_id"] + self.add_site_to_representation(project_name, + repre_id, + data["site_name"], + is_main=True) - add_ids = [repre_id] if family == "workfile": links = get_linked_ids_for_representations(project_name, add_ids, link_type="reference") - add_ids.extend(links) - - add_ids = set(add_ids) - self.log.info("Add to repre_ids {}".format(add_ids)) - is_main = True - for add_repre_id in add_ids: - self.add_site_to_representation(project_name, - add_repre_id, - data["site_name"], - is_main) - is_main = False + for link_repre_id in links: + self.add_site_to_representation(project_name, + link_repre_id, + data["site_name"], + is_main=False) self.log.debug("Site added.") From 6b6c466d8b6ca5b587c8ccf1a8e1dac5e9326bfe Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 12:00:48 +0200 Subject: [PATCH 087/291] OP-2951 - fix wrong variable --- openpype/plugins/load/add_site.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/plugins/load/add_site.py b/openpype/plugins/load/add_site.py index e26ef586e0..22d3ebf24b 100644 --- a/openpype/plugins/load/add_site.py +++ b/openpype/plugins/load/add_site.py @@ -45,7 +45,7 @@ class AddSyncSite(load.LoaderPlugin): if family == "workfile": links = get_linked_ids_for_representations(project_name, - add_ids, + [repre_id], link_type="reference") for link_repre_id in links: self.add_site_to_representation(project_name, From af079897a884538a5af90bbf2301a004fef7233c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 12:07:43 +0200 Subject: [PATCH 088/291] OP-2951 - refactor use better function --- openpype/lib/avalon_context.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index e8a365ec39..496b55a6f2 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -2040,14 +2040,14 @@ def get_linked_ids_for_representations(project, repre_ids, dbcon=None, referenced_version_ids = _process_referenced_pipeline_result(result, link_type) - representations = avalon.io.find( - { + ref_ids = avalon.io.distinct( + "_id", + filter={ "parent": {"$in": list(referenced_version_ids)}, "type": "representation" - }, - projection={"_id": True} + } ) - ref_ids = {representation["_id"] for representation in representations} + return list(ref_ids) From b826cfac4115f51d8387daab30e3475445256e0f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 31 Mar 2022 12:14:17 +0200 Subject: [PATCH 089/291] OP-2951 - change sort by depth Previous sorting by _id might not be deterministic, not reliable. The main logic is to have outputs sorted by how they were traversed, which should be denoted by 'depth' field. --- openpype/lib/avalon_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 496b55a6f2..9a5d382c98 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -2067,7 +2067,7 @@ def _process_referenced_pipeline_result(result, link_type): # outputs_recursive in random order, sort by _id outputs_recursive = sorted(item.get("outputs_recursive", []), - key=lambda d: d["_id"]) + key=lambda d: d["depth"]) # go from oldest to newest # only older _id can reference another newer _id for output in outputs_recursive[::-1]: From d8c56f0a67cacfc2e05b726efc0e3d8e392c0f78 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 10:39:52 +0200 Subject: [PATCH 090/291] Update openpype/lib/avalon_context.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/avalon_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 9a5d382c98..5ea472f11e 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -2001,7 +2001,7 @@ def get_linked_ids_for_representations(project, repre_ids, dbcon=None, if not isinstance(repre_ids, list): repre_ids = [repre_ids] - versions = avalon.io.find( + versions = dbcon.find( { "_id": {"$in": repre_ids}, "type": "representation" From 6f86f78860c795f027ac481b1f6494ddd5b6979c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 10:40:00 +0200 Subject: [PATCH 091/291] Update openpype/lib/avalon_context.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/avalon_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 5ea472f11e..68d38acf35 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -2040,7 +2040,7 @@ def get_linked_ids_for_representations(project, repre_ids, dbcon=None, referenced_version_ids = _process_referenced_pipeline_result(result, link_type) - ref_ids = avalon.io.distinct( + ref_ids = dbcon.distinct( "_id", filter={ "parent": {"$in": list(referenced_version_ids)}, From d14d739e1cfd312390d9ab880da0a589b3c6d567 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 10:40:08 +0200 Subject: [PATCH 092/291] Update openpype/lib/avalon_context.py Co-authored-by: Jakub Trllo <43494761+iLLiCiTiT@users.noreply.github.com> --- openpype/lib/avalon_context.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 68d38acf35..7d562733fc 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1974,7 +1974,7 @@ def get_last_workfile( @with_avalon -def get_linked_ids_for_representations(project, repre_ids, dbcon=None, +def get_linked_ids_for_representations(project_name, repre_ids, dbcon=None, link_type=None, max_depth=0): """Returns list of linked ids of particular type (if provided). From 44afe82d5a21f8ac4bf393fa35b2357df0c583a5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 11:07:54 +0200 Subject: [PATCH 093/291] OP-2951 - refactored distinct version ids Fixed ordering of referenced versions --- openpype/lib/avalon_context.py | 37 +++++++++++++++------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 7d562733fc..65575493e0 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1980,7 +1980,7 @@ def get_linked_ids_for_representations(project_name, repre_ids, dbcon=None, Goes from representations to version, back to representations Args: - project (str) + project_name (str) repre_ids (list) or (ObjectId) dbcon (avalon.mongodb.AvalonMongoDB, optional): Avalon Mongo connection with Session. @@ -1995,23 +1995,24 @@ def get_linked_ids_for_representations(project_name, repre_ids, dbcon=None, # Make sure is installed dbcon.install() - if dbcon.Session["AVALON_PROJECT"] != project: - dbcon.Session["AVALON_PROJECT"] = project + if dbcon.Session["AVALON_PROJECT"] != project_name: + dbcon.Session["AVALON_PROJECT"] = project_name if not isinstance(repre_ids, list): repre_ids = [repre_ids] - versions = dbcon.find( - { - "_id": {"$in": repre_ids}, - "type": "representation" - }, - projection={"parent": True} - ) - version_ids = [version["parent"] for version in versions] + version_ids = dbcon.distinct("parent", { + "_id": {"$in": repre_ids}, + "type": "representation" + }) + + match = { + "_id": {"$in": version_ids}, + "type": "version" + } graph_lookup = { - "from": project, + "from": project_name, "startWith": "$data.inputLinks.id", "connectFromField": "data.inputLinks.id", "connectToField": "_id", @@ -2024,11 +2025,6 @@ def get_linked_ids_for_representations(project_name, repre_ids, dbcon=None, # for outputs. graph_lookup["maxDepth"] = max_depth - 1 - match = { - "_id": {"$in": version_ids}, - "type": "version" - } - pipeline_ = [ # Match {"$match": match}, @@ -2065,12 +2061,11 @@ def _process_referenced_pipeline_result(result, link_type): link_type, correctly_linked_ids) - # outputs_recursive in random order, sort by _id + # outputs_recursive in random order, sort by depth outputs_recursive = sorted(item.get("outputs_recursive", []), key=lambda d: d["depth"]) - # go from oldest to newest - # only older _id can reference another newer _id - for output in outputs_recursive[::-1]: + + for output in outputs_recursive: if output["_id"] not in correctly_linked_ids: # leaf continue From 2694d9d557633e06ed51f684e30056c443a4a401 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 9 Mar 2022 10:20:58 +0100 Subject: [PATCH 094/291] OP-2868 - added configuration for default variant value to Settings --- .../plugins/create/create_render.py | 12 +++++++++- .../project_settings/aftereffects.json | 7 ++++++ .../schema_project_aftereffects.json | 23 +++++++++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/aftereffects/plugins/create/create_render.py b/openpype/hosts/aftereffects/plugins/create/create_render.py index c43ada84b5..aee660673b 100644 --- a/openpype/hosts/aftereffects/plugins/create/create_render.py +++ b/openpype/hosts/aftereffects/plugins/create/create_render.py @@ -18,6 +18,16 @@ class RenderCreator(Creator): create_allow_context_change = False + def __init__( + self, create_context, system_settings, project_settings, headless=False + ): + super(RenderCreator, self).__init__(create_context, system_settings, + project_settings, headless) + self._default_variants = (project_settings["aftereffects"] + ["create"] + ["RenderCreator"] + ["defaults"]) + def get_icon(self): return resources.get_openpype_splash_filepath() @@ -79,7 +89,7 @@ class RenderCreator(Creator): self._add_instance_to_context(new_instance) def get_default_variants(self): - return ["Main"] + return self._default_variants def get_instance_attr_defs(self): return [BoolDef("farm", label="Render on farm")] diff --git a/openpype/settings/defaults/project_settings/aftereffects.json b/openpype/settings/defaults/project_settings/aftereffects.json index 6a9a399069..8083aa0972 100644 --- a/openpype/settings/defaults/project_settings/aftereffects.json +++ b/openpype/settings/defaults/project_settings/aftereffects.json @@ -1,4 +1,11 @@ { + "create": { + "RenderCreator": { + "defaults": [ + "Main" + ] + } + }, "publish": { "ValidateSceneSettings": { "enabled": true, diff --git a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json index 4c4cd225ab..1a3eaef540 100644 --- a/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json +++ b/openpype/settings/entities/schemas/projects_schema/schema_project_aftereffects.json @@ -5,6 +5,29 @@ "label": "AfterEffects", "is_file": true, "children": [ + { + "type": "dict", + "collapsible": true, + "key": "create", + "label": "Creator plugins", + "children": [ + { + "type": "dict", + "collapsible": true, + "key": "RenderCreator", + "label": "Create render", + "children": [ + { + "type": "list", + "key": "defaults", + "label": "Default Variants", + "object_type": "text", + "docstring": "Fill default variant(s) (like 'Main' or 'Default') used in subset name creation." + } + ] + } + ] + }, { "type": "dict", "collapsible": true, From 55246ce4a77e25b6d8f7479f741b64839213f5a2 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 17:30:28 +0200 Subject: [PATCH 095/291] Update openpype/lib/avalon_context.py Co-authored-by: Roy Nieterau --- openpype/lib/avalon_context.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/lib/avalon_context.py b/openpype/lib/avalon_context.py index 65575493e0..224d8129a7 100644 --- a/openpype/lib/avalon_context.py +++ b/openpype/lib/avalon_context.py @@ -1995,8 +1995,7 @@ def get_linked_ids_for_representations(project_name, repre_ids, dbcon=None, # Make sure is installed dbcon.install() - if dbcon.Session["AVALON_PROJECT"] != project_name: - dbcon.Session["AVALON_PROJECT"] = project_name + dbcon.Session["AVALON_PROJECT"] = project_name if not isinstance(repre_ids, list): repre_ids = [repre_ids] From 80ee8c523ad20df67ddfd763933b47fc4e6a3b0d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 18:07:47 +0200 Subject: [PATCH 096/291] OP-2766 - clean up logging --- openpype/hosts/photoshop/plugins/create/workfile_creator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/create/workfile_creator.py b/openpype/hosts/photoshop/plugins/create/workfile_creator.py index 2a2fda3cc4..d66a05cad7 100644 --- a/openpype/hosts/photoshop/plugins/create/workfile_creator.py +++ b/openpype/hosts/photoshop/plugins/create/workfile_creator.py @@ -15,7 +15,6 @@ class PSWorkfileCreator(AutoCreator): return [] def collect_instances(self): - print("coll::{}".format(api.list_instances())) for instance_data in api.list_instances(): creator_id = instance_data.get("creator_identifier") if creator_id == self.identifier: @@ -30,7 +29,6 @@ class PSWorkfileCreator(AutoCreator): pass def create(self, options=None): - print("create") existing_instance = None for instance in self.create_context.instances: if instance.family == self.family: From 4cdcb974b384ec628e83f480a24cdca4ddd1e605 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:07:51 +0200 Subject: [PATCH 097/291] removed usage of config callback in maya --- openpype/hosts/maya/api/pipeline.py | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/openpype/hosts/maya/api/pipeline.py b/openpype/hosts/maya/api/pipeline.py index a8834d1ea3..f6f3472eef 100644 --- a/openpype/hosts/maya/api/pipeline.py +++ b/openpype/hosts/maya/api/pipeline.py @@ -9,8 +9,6 @@ import maya.api.OpenMaya as om import pyblish.api import avalon.api -from avalon.lib import find_submodule - import openpype.hosts.maya from openpype.tools.utils import host_tools from openpype.lib import ( @@ -20,7 +18,6 @@ from openpype.lib import ( ) from openpype.lib.path_tools import HostDirmap from openpype.pipeline import ( - LegacyCreator, register_loader_plugin_path, register_inventory_action_path, register_creator_plugin_path, @@ -270,21 +267,8 @@ def ls(): """ container_names = _ls() - - has_metadata_collector = False - config_host = find_submodule(avalon.api.registered_config(), "maya") - if hasattr(config_host, "collect_container_metadata"): - has_metadata_collector = True - for container in sorted(container_names): - data = parse_container(container) - - # Collect custom data if attribute is present - if has_metadata_collector: - metadata = config_host.collect_container_metadata(container) - data.update(metadata) - - yield data + yield parse_container(container) def containerise(name, From b652170492496a7d64caa34b66db334011a1cdde Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:09:05 +0200 Subject: [PATCH 098/291] removed usage of avalon config from houdini --- openpype/hosts/houdini/api/pipeline.py | 16 +--------------- 1 file changed, 1 insertion(+), 15 deletions(-) diff --git a/openpype/hosts/houdini/api/pipeline.py b/openpype/hosts/houdini/api/pipeline.py index 8e093a89bc..6a69814e2e 100644 --- a/openpype/hosts/houdini/api/pipeline.py +++ b/openpype/hosts/houdini/api/pipeline.py @@ -7,8 +7,6 @@ import hou import hdefereval import pyblish.api -import avalon.api -from avalon.lib import find_submodule from openpype.pipeline import ( register_creator_plugin_path, @@ -215,24 +213,12 @@ def ls(): "pyblish.mindbender.container"): containers += lib.lsattr("id", identifier) - has_metadata_collector = False - config_host = find_submodule(avalon.api.registered_config(), "houdini") - if hasattr(config_host, "collect_container_metadata"): - has_metadata_collector = True - for container in sorted(containers, # Hou 19+ Python 3 hou.ObjNode are not # sortable due to not supporting greater # than comparisons key=lambda node: node.path()): - data = parse_container(container) - - # Collect custom data if attribute is present - if has_metadata_collector: - metadata = config_host.collect_container_metadata(container) - data.update(metadata) - - yield data + yield parse_container(container) def before_save(): From 9efa30d7569f0025cdd8a2d1f9a970edfdbb1aad Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 18:09:38 +0200 Subject: [PATCH 099/291] OP-2766 - revert unwanted commit --- .../aftereffects/plugins/publish/collect_workfile.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py index 1983851028..c1c2be4855 100644 --- a/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py +++ b/openpype/hosts/aftereffects/plugins/publish/collect_workfile.py @@ -38,13 +38,7 @@ class CollectWorkfile(pyblish.api.ContextPlugin): # workfile instance family = "workfile" - subset = get_subset_name( - family, - "", - task, - context.data["assetEntity"]["_id"], - host_name="photoshop" - ) + subset = family + task.capitalize() # Create instance instance = context.create_instance(subset) From d92ccf8c2ee97e38d236cd20764f9a3432a3e1a3 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 1 Apr 2022 18:11:26 +0200 Subject: [PATCH 100/291] OP-2766 - cleanup logging --- openpype/hosts/photoshop/plugins/publish/extract_image.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/hosts/photoshop/plugins/publish/extract_image.py b/openpype/hosts/photoshop/plugins/publish/extract_image.py index 75e6323da7..a133e33409 100644 --- a/openpype/hosts/photoshop/plugins/publish/extract_image.py +++ b/openpype/hosts/photoshop/plugins/publish/extract_image.py @@ -16,8 +16,6 @@ class ExtractImage(openpype.api.Extractor): formats = ["png", "jpg"] def process(self, instance): - print("PPPPPP") - self.log.info("fdfdsfdfs") staging_dir = self.staging_dir(instance) self.log.info("Outputting image to {}".format(staging_dir)) From f3f06444ac7ddd193932549d00dc97a2d827d306 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 18:31:48 +0200 Subject: [PATCH 101/291] created process_context with installation functions --- openpype/pipeline/process_context.py | 333 +++++++++++++++++++++++++++ 1 file changed, 333 insertions(+) create mode 100644 openpype/pipeline/process_context.py diff --git a/openpype/pipeline/process_context.py b/openpype/pipeline/process_context.py new file mode 100644 index 0000000000..65e891c100 --- /dev/null +++ b/openpype/pipeline/process_context.py @@ -0,0 +1,333 @@ +"""Core pipeline functionality""" + +import os +import sys +import json +import types +import logging +import inspect +import platform + +import pyblish.api +from pyblish.lib import MessageHandler + +from avalon import io, Session + +import openpype +from openpype.modules import load_modules +from openpype.settings import get_project_settings +from openpype.lib import ( + Anatomy, + register_event_callback, + filter_pyblish_plugins, + change_timer_to_current_context, +) + +from . import ( + register_loader_plugin_path, + register_inventory_action, + register_creator_plugin_path, + deregister_loader_plugin_path, +) + + +_is_installed = False +_registered_root = {"_": ""} +_registered_host = {"_": None} + +log = logging.getLogger(__name__) + +PACKAGE_DIR = os.path.dirname(os.path.abspath(openpype.__file__)) +PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") + +# Global plugin paths +PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") +LOAD_PATH = os.path.join(PLUGINS_DIR, "load") + + +def register_root(path): + """Register currently active root""" + log.info("Registering root: %s" % path) + _registered_root["_"] = path + + +def registered_root(): + """Return currently registered root""" + root = _registered_root["_"] + if root: + return root + + root = Session.get("AVALON_PROJECTS") + if root: + return os.path.normpath(root) + return "" + + +def install(host): + """Install `host` into the running Python session. + + Args: + host (module): A Python module containing the Avalon + avalon host-interface. + """ + global _is_installed + + io.install() + + missing = list() + for key in ("AVALON_PROJECT", "AVALON_ASSET"): + if key not in Session: + missing.append(key) + + assert not missing, ( + "%s missing from environment, %s" % ( + ", ".join(missing), + json.dumps(Session, indent=4, sort_keys=True) + )) + + project_name = Session["AVALON_PROJECT"] + log.info("Activating %s.." % project_name) + + # Optional host install function + if hasattr(host, "install"): + host.install() + + register_host(host) + + _is_installed = True + + # Make sure modules are loaded + load_modules() + + def modified_emit(obj, record): + """Method replacing `emit` in Pyblish's MessageHandler.""" + record.msg = record.getMessage() + obj.records.append(record) + + MessageHandler.emit = modified_emit + + log.info("Registering global plug-ins..") + pyblish.api.register_plugin_path(PUBLISH_PATH) + pyblish.api.register_discovery_filter(filter_pyblish_plugins) + register_loader_plugin_path(LOAD_PATH) + + project_name = os.environ.get("AVALON_PROJECT") + + # Register studio specific plugins + if project_name: + anatomy = Anatomy(project_name) + anatomy.set_root_environments() + register_root(anatomy.roots) + + project_settings = get_project_settings(project_name) + platform_name = platform.system().lower() + project_plugins = ( + project_settings + .get("global", {}) + .get("project_plugins", {}) + .get(platform_name) + ) or [] + for path in project_plugins: + try: + path = str(path.format(**os.environ)) + except KeyError: + pass + + if not path or not os.path.exists(path): + continue + + pyblish.api.register_plugin_path(path) + register_loader_plugin_path(path) + register_creator_plugin_path(path) + register_inventory_action(path) + + # apply monkey patched discover to original one + log.info("Patching discovery") + + register_event_callback("taskChanged", _on_task_change) + + +def _on_task_change(): + change_timer_to_current_context() + + +def uninstall(): + """Undo all of what `install()` did""" + host = registered_host() + + try: + host.uninstall() + except AttributeError: + pass + + log.info("Deregistering global plug-ins..") + pyblish.api.deregister_plugin_path(PUBLISH_PATH) + pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) + deregister_loader_plugin_path(LOAD_PATH) + log.info("Global plug-ins unregistred") + + deregister_host() + + io.uninstall() + + log.info("Successfully uninstalled Avalon!") + + +def is_installed(): + """Return state of installation + + Returns: + True if installed, False otherwise + + """ + + return _is_installed + + +def register_host(host): + """Register a new host for the current process + + Arguments: + host (ModuleType): A module implementing the + Host API interface. See the Host API + documentation for details on what is + required, or browse the source code. + + """ + signatures = { + "ls": [] + } + + _validate_signature(host, signatures) + _registered_host["_"] = host + + +def _validate_signature(module, signatures): + # Required signatures for each member + + missing = list() + invalid = list() + success = True + + for member in signatures: + if not hasattr(module, member): + missing.append(member) + success = False + + else: + attr = getattr(module, member) + if sys.version_info.major >= 3: + signature = inspect.getfullargspec(attr)[0] + else: + signature = inspect.getargspec(attr)[0] + required_signature = signatures[member] + + assert isinstance(signature, list) + assert isinstance(required_signature, list) + + if not all(member in signature + for member in required_signature): + invalid.append({ + "member": member, + "signature": ", ".join(signature), + "required": ", ".join(required_signature) + }) + success = False + + if not success: + report = list() + + if missing: + report.append( + "Incomplete interface for module: '%s'\n" + "Missing: %s" % (module, ", ".join( + "'%s'" % member for member in missing)) + ) + + if invalid: + report.append( + "'%s': One or more members were found, but didn't " + "have the right argument signature." % module.__name__ + ) + + for member in invalid: + report.append( + " Found: {member}({signature})".format(**member) + ) + report.append( + " Expected: {member}({required})".format(**member) + ) + + raise ValueError("\n".join(report)) + + +def registered_host(): + """Return currently registered host""" + return _registered_host["_"] + + +def deregister_host(): + _registered_host["_"] = default_host() + + +def default_host(): + """A default host, in place of anything better + + This may be considered as reference for the + interface a host must implement. It also ensures + that the system runs, even when nothing is there + to support it. + + """ + + host = types.ModuleType("defaultHost") + + def ls(): + return list() + + host.__dict__.update({ + "ls": ls + }) + + return host + + +def debug_host(): + """A debug host, useful to debugging features that depend on a host""" + + host = types.ModuleType("debugHost") + + def ls(): + containers = [ + { + "representation": "ee-ft-a-uuid1", + "schema": "openpype:container-1.0", + "name": "Bruce01", + "objectName": "Bruce01_node", + "namespace": "_bruce01_", + "version": 3, + }, + { + "representation": "aa-bc-s-uuid2", + "schema": "openpype:container-1.0", + "name": "Bruce02", + "objectName": "Bruce01_node", + "namespace": "_bruce02_", + "version": 2, + } + ] + + for container in containers: + yield container + + host.__dict__.update({ + "ls": ls, + "open_file": lambda fname: None, + "save_file": lambda fname: None, + "current_file": lambda: os.path.expanduser("~/temp.txt"), + "has_unsaved_changes": lambda: False, + "work_root": lambda: os.path.expanduser("~/temp"), + "file_extensions": lambda: ["txt"], + }) + + return host From c49791258a5b714c84085f465e05b43f72f08266 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:36:08 +0200 Subject: [PATCH 102/291] changed function names and separated install in 2 parts --- openpype/pipeline/__init__.py | 33 ++++++++++++++++++++++++++++ openpype/pipeline/process_context.py | 26 ++++++++++++---------- 2 files changed, 47 insertions(+), 12 deletions(-) diff --git a/openpype/pipeline/__init__.py b/openpype/pipeline/__init__.py index 8460d20ef1..914606cc2f 100644 --- a/openpype/pipeline/__init__.py +++ b/openpype/pipeline/__init__.py @@ -68,6 +68,22 @@ from .actions import ( deregister_inventory_action_path, ) +from .process_context import ( + install_openpype_plugins, + install_host, + uninstall_host, + is_installed, + + register_root, + registered_root, + + register_host, + registered_host, + deregister_host, +) +install = install_host +uninstall = uninstall_host + __all__ = ( "AVALON_CONTAINER_ID", @@ -135,4 +151,21 @@ __all__ = ( "register_inventory_action_path", "deregister_inventory_action", "deregister_inventory_action_path", + + # --- Process context --- + "install_openpype_plugins", + "install_host", + "uninstall_host", + "is_installed", + + "register_root", + "registered_root", + + "register_host", + "registered_host", + "deregister_host", + + # Backwards compatible function names + "install", + "uninstall", ) diff --git a/openpype/pipeline/process_context.py b/openpype/pipeline/process_context.py index 65e891c100..1bef260ec9 100644 --- a/openpype/pipeline/process_context.py +++ b/openpype/pipeline/process_context.py @@ -63,7 +63,7 @@ def registered_root(): return "" -def install(host): +def install_host(host): """Install `host` into the running Python session. Args: @@ -72,6 +72,8 @@ def install(host): """ global _is_installed + _is_installed = True + io.install() missing = list() @@ -94,10 +96,7 @@ def install(host): register_host(host) - _is_installed = True - - # Make sure modules are loaded - load_modules() + register_event_callback("taskChanged", _on_task_change) def modified_emit(obj, record): """Method replacing `emit` in Pyblish's MessageHandler.""" @@ -106,12 +105,20 @@ def install(host): MessageHandler.emit = modified_emit + install_openpype_plugins() + + +def install_openpype_plugins(project_name=None): + # Make sure modules are loaded + load_modules() + log.info("Registering global plug-ins..") pyblish.api.register_plugin_path(PUBLISH_PATH) pyblish.api.register_discovery_filter(filter_pyblish_plugins) register_loader_plugin_path(LOAD_PATH) - project_name = os.environ.get("AVALON_PROJECT") + if project_name is None: + project_name = os.environ.get("AVALON_PROJECT") # Register studio specific plugins if project_name: @@ -141,17 +148,12 @@ def install(host): register_creator_plugin_path(path) register_inventory_action(path) - # apply monkey patched discover to original one - log.info("Patching discovery") - - register_event_callback("taskChanged", _on_task_change) - def _on_task_change(): change_timer_to_current_context() -def uninstall(): +def uninstall_host(): """Undo all of what `install()` did""" host = registered_host() From eabe2fe56960a098608b10599e5d0c1cee550f9a Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:37:58 +0200 Subject: [PATCH 103/291] changed usage of registered root --- .../vendor/husdoutputprocessors/avalon_uri_processor.py | 3 ++- openpype/lib/usdlib.py | 3 ++- openpype/pipeline/load/utils.py | 4 +++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py index 499b733570..8cd51e6641 100644 --- a/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py +++ b/openpype/hosts/houdini/vendor/husdoutputprocessors/avalon_uri_processor.py @@ -134,6 +134,7 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): """ from avalon import api, io + from openpype.pipeline import registered_root PROJECT = api.Session["AVALON_PROJECT"] asset_doc = io.find_one({"name": asset, @@ -141,7 +142,7 @@ class AvalonURIOutputProcessor(base.OutputProcessorBase): if not asset_doc: raise RuntimeError("Invalid asset name: '%s'" % asset) - root = api.registered_root() + root = registered_root() path = self._template.format(**{ "root": root, "project": PROJECT, diff --git a/openpype/lib/usdlib.py b/openpype/lib/usdlib.py index 89021156b4..7b3b7112de 100644 --- a/openpype/lib/usdlib.py +++ b/openpype/lib/usdlib.py @@ -9,6 +9,7 @@ except ImportError: from mvpxr import Usd, UsdGeom, Sdf, Kind from avalon import io, api +from openpype.pipeline import registered_root log = logging.getLogger(__name__) @@ -323,7 +324,7 @@ def get_usd_master_path(asset, subset, representation): path = template.format( **{ - "root": api.registered_root(), + "root": registered_root(), "project": api.Session["AVALON_PROJECT"], "asset": asset_doc["name"], "subset": subset, diff --git a/openpype/pipeline/load/utils.py b/openpype/pipeline/load/utils.py index 53ac6b626d..cb7c76f133 100644 --- a/openpype/pipeline/load/utils.py +++ b/openpype/pipeline/load/utils.py @@ -10,7 +10,7 @@ import six from bson.objectid import ObjectId from avalon import io, schema -from avalon.api import Session, registered_root +from avalon.api import Session from openpype.lib import Anatomy @@ -532,6 +532,8 @@ def get_representation_path(representation, root=None, dbcon=None): dbcon = io if root is None: + from openpype.pipeline import registered_root + root = registered_root() def path_from_represenation(): From 729131738a5ef8d618f3877da2bb4635e0c2d8be Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:40:23 +0200 Subject: [PATCH 104/291] changed installation of hosts --- openpype/hosts/aftereffects/api/lib.py | 4 ++-- openpype/hosts/blender/api/pipeline.py | 4 ++-- .../hosts/blender/blender_addon/startup/init.py | 4 ++-- openpype/hosts/celaction/api/cli.py | 10 ++++------ openpype/hosts/flame/startup/openpype_in_flame.py | 6 +++--- openpype/hosts/fusion/scripts/fusion_switch_shot.py | 8 ++++++-- .../fusion/utility_scripts/__OpenPype_Menu__.py | 13 ++++++------- openpype/hosts/fusion/utility_scripts/switch_ui.py | 6 +++--- openpype/hosts/harmony/api/lib.py | 4 ++-- openpype/hosts/hiero/api/pipeline.py | 9 +-------- .../hiero/api/startup/Python/Startup/Startup.py | 4 ++-- .../hosts/houdini/startup/python2.7libs/pythonrc.py | 4 ++-- .../hosts/houdini/startup/python3.7libs/pythonrc.py | 4 ++-- openpype/hosts/maya/startup/userSetup.py | 5 ++--- openpype/hosts/nuke/startup/menu.py | 4 ++-- openpype/hosts/photoshop/api/lib.py | 5 ++--- .../utility_scripts/OpenPype_sync_util_scripts.py | 5 +++-- .../resolve/utility_scripts/__OpenPype__Menu__.py | 9 ++------- .../utility_scripts/tests/test_otio_as_edl.py | 10 +++++----- .../tests/testing_create_timeline_item_from_path.py | 8 +++----- .../tests/testing_load_media_pool_item.py | 8 +++----- openpype/hosts/tvpaint/api/launch_script.py | 4 ++-- openpype/hosts/tvpaint/api/pipeline.py | 11 ++++------- .../integration/Content/Python/init_unreal.py | 10 ++-------- openpype/hosts/webpublisher/api/__init__.py | 1 - openpype/lib/remote_publish.py | 7 ++----- openpype/tests/test_avalon_plugin_presets.py | 12 +++++------- 27 files changed, 74 insertions(+), 105 deletions(-) diff --git a/openpype/hosts/aftereffects/api/lib.py b/openpype/hosts/aftereffects/api/lib.py index dac6b5d28f..ce4cbf09af 100644 --- a/openpype/hosts/aftereffects/api/lib.py +++ b/openpype/hosts/aftereffects/api/lib.py @@ -6,6 +6,7 @@ import logging from Qt import QtWidgets +from openpype.pipeline import install_host from openpype.lib.remote_publish import headless_publish from openpype.tools.utils import host_tools @@ -22,10 +23,9 @@ def safe_excepthook(*args): def main(*subprocess_args): sys.excepthook = safe_excepthook - import avalon.api from openpype.hosts.aftereffects import api - avalon.api.install(api) + install_host(api) os.environ["OPENPYPE_LOG_NO_COLORS"] = "False" app = QtWidgets.QApplication([]) diff --git a/openpype/hosts/blender/api/pipeline.py b/openpype/hosts/blender/api/pipeline.py index b9ec2cfea4..0ea579970e 100644 --- a/openpype/hosts/blender/api/pipeline.py +++ b/openpype/hosts/blender/api/pipeline.py @@ -19,6 +19,7 @@ from openpype.pipeline import ( deregister_loader_plugin_path, deregister_creator_plugin_path, AVALON_CONTAINER_ID, + uninstall_host, ) from openpype.api import Logger from openpype.lib import ( @@ -209,11 +210,10 @@ def reload_pipeline(*args): """ - avalon.api.uninstall() + uninstall_host() for module in ( "avalon.io", - "avalon.lib", "avalon.pipeline", "avalon.api", ): diff --git a/openpype/hosts/blender/blender_addon/startup/init.py b/openpype/hosts/blender/blender_addon/startup/init.py index e43373bc6c..13a4b8a7a1 100644 --- a/openpype/hosts/blender/blender_addon/startup/init.py +++ b/openpype/hosts/blender/blender_addon/startup/init.py @@ -1,4 +1,4 @@ -from avalon import pipeline +from openpype.pipeline import install_host from openpype.hosts.blender import api -pipeline.install(api) +install_host(api) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index bc1e3eaf89..85e210f21a 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -3,8 +3,6 @@ import sys import copy import argparse -from avalon import io - import pyblish.api import pyblish.util @@ -13,6 +11,8 @@ import openpype import openpype.hosts.celaction from openpype.hosts.celaction import api as celaction from openpype.tools.utils import host_tools +from openpype.pipeline.process_context import install_openpype_plugins + log = Logger().get_logger("Celaction_cli_publisher") @@ -21,9 +21,6 @@ publish_host = "celaction" HOST_DIR = os.path.dirname(os.path.abspath(openpype.hosts.celaction.__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") -INVENTORY_PATH = os.path.join(PLUGINS_DIR, "inventory") def cli(): @@ -74,7 +71,8 @@ def main(): _prepare_publish_environments() # Registers pype's Global pyblish plugins - openpype.install() + # - use fake host + install_openpype_plugins() if os.path.exists(PUBLISH_PATH): log.info(f"Registering path: {PUBLISH_PATH}") diff --git a/openpype/hosts/flame/startup/openpype_in_flame.py b/openpype/hosts/flame/startup/openpype_in_flame.py index 931c5a1b79..7015abc7f4 100644 --- a/openpype/hosts/flame/startup/openpype_in_flame.py +++ b/openpype/hosts/flame/startup/openpype_in_flame.py @@ -3,16 +3,16 @@ import sys from Qt import QtWidgets from pprint import pformat import atexit -import openpype + import avalon import openpype.hosts.flame.api as opfapi +from openpype.pipeline import install_host def openpype_install(): """Registering OpenPype in context """ - openpype.install() - avalon.api.install(opfapi) + install_host(opfapi) print("Avalon registered hosts: {}".format( avalon.api.registered_host())) diff --git a/openpype/hosts/fusion/scripts/fusion_switch_shot.py b/openpype/hosts/fusion/scripts/fusion_switch_shot.py index ca7efb9136..ca8e5c9e37 100644 --- a/openpype/hosts/fusion/scripts/fusion_switch_shot.py +++ b/openpype/hosts/fusion/scripts/fusion_switch_shot.py @@ -7,6 +7,10 @@ import logging import avalon.api from avalon import io +from openpype.pipeline import ( + install_host, + registered_host, +) from openpype.lib import version_up from openpype.hosts.fusion import api from openpype.hosts.fusion.api import lib @@ -218,7 +222,7 @@ def switch(asset_name, filepath=None, new=True): assert current_comp is not None, ( "Fusion could not load '{}'").format(filepath) - host = avalon.api.registered_host() + host = registered_host() containers = list(host.ls()) assert containers, "Nothing to update" @@ -279,7 +283,7 @@ if __name__ == '__main__': args, unknown = parser.parse_args() - avalon.api.install(api) + install_host(api) switch(args.asset_name, args.file_path) sys.exit(0) diff --git a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py index 4b5e8f91a0..aa98563785 100644 --- a/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py +++ b/openpype/hosts/fusion/utility_scripts/__OpenPype_Menu__.py @@ -1,24 +1,23 @@ import os import sys -import openpype from openpype.api import Logger +from openpype.pipeline import ( + install_host, + registered_host, +) log = Logger().get_logger(__name__) def main(env): - import avalon.api from openpype.hosts.fusion import api from openpype.hosts.fusion.api import menu - # Registers pype's Global pyblish plugins - openpype.install() - # activate resolve from pype - avalon.api.install(api) + install_host(api) - log.info(f"Avalon registered hosts: {avalon.api.registered_host()}") + log.info(f"Avalon registered hosts: {registered_host()}") menu.launch_openpype_menu() diff --git a/openpype/hosts/fusion/utility_scripts/switch_ui.py b/openpype/hosts/fusion/utility_scripts/switch_ui.py index d9eeae25ea..37306c7a2a 100644 --- a/openpype/hosts/fusion/utility_scripts/switch_ui.py +++ b/openpype/hosts/fusion/utility_scripts/switch_ui.py @@ -1,14 +1,15 @@ import os +import sys import glob import logging from Qt import QtWidgets, QtCore -import avalon.api from avalon import io import qtawesome as qta from openpype import style +from openpype.pipeline import install_host from openpype.hosts.fusion import api from openpype.lib.avalon_context import get_workdir_from_session @@ -181,8 +182,7 @@ class App(QtWidgets.QWidget): if __name__ == '__main__': - import sys - avalon.api.install(api) + install_host(api) app = QtWidgets.QApplication(sys.argv) window = App() diff --git a/openpype/hosts/harmony/api/lib.py b/openpype/hosts/harmony/api/lib.py index 66eeac1e3a..53fd0f07dd 100644 --- a/openpype/hosts/harmony/api/lib.py +++ b/openpype/hosts/harmony/api/lib.py @@ -183,10 +183,10 @@ def launch(application_path, *args): application_path (str): Path to Harmony. """ - from avalon import api + from openpype.pipeline import install_host from openpype.hosts.harmony import api as harmony - api.install(harmony) + install_host(harmony) ProcessContext.port = random.randrange(49152, 65535) os.environ["AVALON_HARMONY_PORT"] = str(ProcessContext.port) diff --git a/openpype/hosts/hiero/api/pipeline.py b/openpype/hosts/hiero/api/pipeline.py index b334102129..616ff53fd8 100644 --- a/openpype/hosts/hiero/api/pipeline.py +++ b/openpype/hosts/hiero/api/pipeline.py @@ -34,14 +34,7 @@ AVALON_CONTAINERS = ":AVALON_CONTAINERS" def install(): - """ - Installing Hiero integration for avalon - - Args: - config (obj): avalon config module `pype` in our case, it is not - used but required by avalon.api.install() - - """ + """Installing Hiero integration.""" # adding all events events.register_events() diff --git a/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py b/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py index 21c21cd7c3..2e638c2088 100644 --- a/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py +++ b/openpype/hosts/hiero/api/startup/Python/Startup/Startup.py @@ -1,9 +1,9 @@ import traceback # activate hiero from pype -import avalon.api +from openpype.pipeline import install_host import openpype.hosts.hiero.api as phiero -avalon.api.install(phiero) +install_host(phiero) try: __import__("openpype.hosts.hiero.api") diff --git a/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py b/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py index eb33b49759..afadbffd3e 100644 --- a/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py +++ b/openpype/hosts/houdini/startup/python2.7libs/pythonrc.py @@ -1,10 +1,10 @@ -import avalon.api +from openpype.pipeline import install_host from openpype.hosts.houdini import api def main(): print("Installing OpenPype ...") - avalon.api.install(api) + install_host(api) main() diff --git a/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py b/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py index eb33b49759..afadbffd3e 100644 --- a/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py +++ b/openpype/hosts/houdini/startup/python3.7libs/pythonrc.py @@ -1,10 +1,10 @@ -import avalon.api +from openpype.pipeline import install_host from openpype.hosts.houdini import api def main(): print("Installing OpenPype ...") - avalon.api.install(api) + install_host(api) main() diff --git a/openpype/hosts/maya/startup/userSetup.py b/openpype/hosts/maya/startup/userSetup.py index b89244817a..a3ab483add 100644 --- a/openpype/hosts/maya/startup/userSetup.py +++ b/openpype/hosts/maya/startup/userSetup.py @@ -1,11 +1,10 @@ import os -import avalon.api from openpype.api import get_project_settings +from openpype.pipeline import install_host from openpype.hosts.maya import api -import openpype.hosts.maya.api.lib as mlib from maya import cmds -avalon.api.install(api) +install_host(api) print("starting OpenPype usersetup") diff --git a/openpype/hosts/nuke/startup/menu.py b/openpype/hosts/nuke/startup/menu.py index 2cac6d09e7..9ed43b2110 100644 --- a/openpype/hosts/nuke/startup/menu.py +++ b/openpype/hosts/nuke/startup/menu.py @@ -1,7 +1,7 @@ import nuke -import avalon.api from openpype.api import Logger +from openpype.pipeline import install_host from openpype.hosts.nuke import api from openpype.hosts.nuke.api.lib import ( on_script_load, @@ -13,7 +13,7 @@ from openpype.hosts.nuke.api.lib import ( log = Logger.get_logger(__name__) -avalon.api.install(api) +install_host(api) # fix ffmpeg settings on script nuke.addOnScriptLoad(on_script_load) diff --git a/openpype/hosts/photoshop/api/lib.py b/openpype/hosts/photoshop/api/lib.py index 6d2a493a94..2f57d64464 100644 --- a/openpype/hosts/photoshop/api/lib.py +++ b/openpype/hosts/photoshop/api/lib.py @@ -5,9 +5,8 @@ import traceback from Qt import QtWidgets -import avalon.api - from openpype.api import Logger +from openpype.pipeline import install_host from openpype.tools.utils import host_tools from openpype.lib.remote_publish import headless_publish from openpype.lib import env_value_to_bool @@ -24,7 +23,7 @@ def safe_excepthook(*args): def main(*subprocess_args): from openpype.hosts.photoshop import api - avalon.api.install(api) + install_host(api) sys.excepthook = safe_excepthook # coloring in StdOutBroker 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 ac66916b91..3a16b9c966 100644 --- a/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py +++ b/openpype/hosts/resolve/utility_scripts/OpenPype_sync_util_scripts.py @@ -1,13 +1,14 @@ #!/usr/bin/env python import os import sys -import openpype + +from openpype.pipeline import install_host def main(env): import openpype.hosts.resolve as bmdvr # Registers openpype's Global pyblish plugins - openpype.install() + install_host(bmdvr) bmdvr.setup(env) diff --git a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py index b0cef1838a..89ade9238b 100644 --- a/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py +++ b/openpype/hosts/resolve/utility_scripts/__OpenPype__Menu__.py @@ -1,8 +1,7 @@ import os import sys -import avalon.api as avalon -import openpype +from openpype.pipeline import install_host from openpype.api import Logger log = Logger().get_logger(__name__) @@ -10,13 +9,9 @@ log = Logger().get_logger(__name__) def main(env): import openpype.hosts.resolve as bmdvr - # Registers openpype's Global pyblish plugins - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) - - log.info(f"Avalon registered hosts: {avalon.registered_host()}") + install_host(bmdvr) bmdvr.launch_pype_menu() 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 5430ad32df..8433bd9172 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 @@ -1,9 +1,11 @@ #! python3 import os import sys -import avalon.api as avalon -import openpype + import opentimelineio as otio + +from openpype.pipeline import install_host + from openpype.hosts.resolve import TestGUI import openpype.hosts.resolve as bmdvr from openpype.hosts.resolve.otio import davinci_export as otio_export @@ -14,10 +16,8 @@ class ThisTestGUI(TestGUI): def __init__(self): super(ThisTestGUI, self).__init__() - # Registers openpype's Global pyblish plugins - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) + install_host(bmdvr) def _open_dir_button_pressed(self, event): # selected_path = self.fu.RequestFile(os.path.expanduser("~")) 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 afa311e0b8..477955d527 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 @@ -1,8 +1,8 @@ #! python3 import os import sys -import avalon.api as avalon -import openpype + +from openpype.pipeline import install_host from openpype.hosts.resolve import TestGUI import openpype.hosts.resolve as bmdvr import clique @@ -13,10 +13,8 @@ class ThisTestGUI(TestGUI): def __init__(self): super(ThisTestGUI, self).__init__() - # Registers openpype's Global pyblish plugins - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) + install_host(bmdvr) def _open_dir_button_pressed(self, event): # selected_path = self.fu.RequestFile(os.path.expanduser("~")) 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 cfdbe890e5..872d620162 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,6 +1,5 @@ #! python3 -import avalon.api as avalon -import openpype +from openpype.pipeline import install_host import openpype.hosts.resolve as bmdvr @@ -15,8 +14,7 @@ def file_processing(fpath): if __name__ == "__main__": path = "C:/CODE/__openpype_projects/jtest03dev/shots/sq01/mainsq01sh030/publish/plate/plateMain/v006/jt3d_mainsq01sh030_plateMain_v006.0996.exr" - openpype.install() # activate resolve from openpype - avalon.install(bmdvr) + install_host(bmdvr) - file_processing(path) \ No newline at end of file + file_processing(path) diff --git a/openpype/hosts/tvpaint/api/launch_script.py b/openpype/hosts/tvpaint/api/launch_script.py index e66bf61df6..0b25027fc6 100644 --- a/openpype/hosts/tvpaint/api/launch_script.py +++ b/openpype/hosts/tvpaint/api/launch_script.py @@ -8,8 +8,8 @@ import logging from Qt import QtWidgets, QtCore, QtGui -from avalon import api from openpype import style +from openpype.pipeline import install_host from openpype.hosts.tvpaint.api.communication_server import ( CommunicationWrapper ) @@ -31,7 +31,7 @@ def main(launch_args): qt_app = QtWidgets.QApplication([]) # Execute pipeline installation - api.install(tvpaint_host) + install_host(tvpaint_host) # Create Communicator object and trigger launch # - this must be done before anything is processed diff --git a/openpype/hosts/tvpaint/api/pipeline.py b/openpype/hosts/tvpaint/api/pipeline.py index cafdf0701d..78c10c3dae 100644 --- a/openpype/hosts/tvpaint/api/pipeline.py +++ b/openpype/hosts/tvpaint/api/pipeline.py @@ -67,11 +67,8 @@ instances=2 def install(): - """Install Maya-specific functionality of avalon-core. + """Install TVPaint-specific functionality.""" - This function is called automatically on calling `api.install(maya)`. - - """ log.info("OpenPype - Installing TVPaint integration") io.install() @@ -96,11 +93,11 @@ def install(): def uninstall(): - """Uninstall TVPaint-specific functionality of avalon-core. - - This function is called automatically on calling `api.uninstall()`. + """Uninstall TVPaint-specific functionality. + This function is called automatically on calling `uninstall_host()`. """ + log.info("OpenPype - Uninstalling TVPaint integration") pyblish.api.deregister_host("tvpaint") pyblish.api.deregister_plugin_path(PUBLISH_PATH) diff --git a/openpype/hosts/unreal/integration/Content/Python/init_unreal.py b/openpype/hosts/unreal/integration/Content/Python/init_unreal.py index 2ecd301c25..4bb03b07ed 100644 --- a/openpype/hosts/unreal/integration/Content/Python/init_unreal.py +++ b/openpype/hosts/unreal/integration/Content/Python/init_unreal.py @@ -2,13 +2,7 @@ import unreal openpype_detected = True try: - from avalon import api -except ImportError as exc: - api = None - openpype_detected = False - unreal.log_error("Avalon: cannot load Avalon [ {} ]".format(exc)) - -try: + from openpype.pipeline import install_host from openpype.hosts.unreal import api as openpype_host except ImportError as exc: openpype_host = None @@ -16,7 +10,7 @@ except ImportError as exc: unreal.log_error("OpenPype: cannot load OpenPype [ {} ]".format(exc)) if openpype_detected: - api.install(openpype_host) + install_host(openpype_host) @unreal.uclass() diff --git a/openpype/hosts/webpublisher/api/__init__.py b/openpype/hosts/webpublisher/api/__init__.py index dbeb628073..72bbffd099 100644 --- a/openpype/hosts/webpublisher/api/__init__.py +++ b/openpype/hosts/webpublisher/api/__init__.py @@ -1,7 +1,6 @@ import os import logging -from avalon import api as avalon from avalon import io from pyblish import api as pyblish import openpype.hosts.webpublisher diff --git a/openpype/lib/remote_publish.py b/openpype/lib/remote_publish.py index 9d97671a61..8a42daf4e9 100644 --- a/openpype/lib/remote_publish.py +++ b/openpype/lib/remote_publish.py @@ -1,13 +1,12 @@ import os from datetime import datetime -import sys -from bson.objectid import ObjectId import collections +from bson.objectid import ObjectId + import pyblish.util import pyblish.api -from openpype import uninstall from openpype.lib.mongo import OpenPypeMongoConnection from openpype.lib.plugin_tools import parse_json @@ -81,7 +80,6 @@ def publish(log, close_plugin_name=None): if result["error"]: log.error(error_format.format(**result)) - uninstall() if close_plugin: # close host app explicitly after error context = pyblish.api.Context() close_plugin().process(context) @@ -118,7 +116,6 @@ def publish_and_log(dbcon, _id, log, close_plugin_name=None, batch_id=None): if result["error"]: log.error(error_format.format(**result)) - uninstall() log_lines = [error_format.format(**result)] + log_lines dbcon.update_one( {"_id": _id}, diff --git a/openpype/tests/test_avalon_plugin_presets.py b/openpype/tests/test_avalon_plugin_presets.py index c491be1c05..464c216d6f 100644 --- a/openpype/tests/test_avalon_plugin_presets.py +++ b/openpype/tests/test_avalon_plugin_presets.py @@ -1,6 +1,5 @@ -import avalon.api as api -import openpype from openpype.pipeline import ( + install_host, LegacyCreator, register_creator_plugin, discover_creator_plugins, @@ -23,15 +22,14 @@ class Test: __name__ = "test" ls = len - def __call__(self): - pass + @staticmethod + def install(): + register_creator_plugin(MyTestCreator) def test_avalon_plugin_presets(monkeypatch, printer): + install_host(Test) - openpype.install() - api.register_host(Test()) - register_creator_plugin(MyTestCreator) plugins = discover_creator_plugins() printer("Test if we got our test plugin") assert MyTestCreator in plugins From 331f87bd15c15099eb30d678c871fe3809dba885 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:18 +0200 Subject: [PATCH 105/291] changed usages of registered host and config --- openpype/hosts/testhost/run_publish.py | 4 ++-- openpype/pype_commands.py | 10 ++++++---- openpype/tools/loader/app.py | 10 ---------- openpype/tools/utils/host_tools.py | 3 ++- openpype/tools/utils/lib.py | 15 ++++++++------- 5 files changed, 18 insertions(+), 24 deletions(-) diff --git a/openpype/hosts/testhost/run_publish.py b/openpype/hosts/testhost/run_publish.py index 44860a30e4..cc80bdc604 100644 --- a/openpype/hosts/testhost/run_publish.py +++ b/openpype/hosts/testhost/run_publish.py @@ -48,8 +48,8 @@ from openpype.tools.publisher.window import PublisherWindow def main(): """Main function for testing purposes.""" - import avalon.api import pyblish.api + from openpype.pipeline import install_host from openpype.modules import ModulesManager from openpype.hosts.testhost import api as testhost @@ -57,7 +57,7 @@ def main(): for plugin_path in manager.collect_plugin_paths()["publish"]: pyblish.api.register_plugin_path(plugin_path) - avalon.api.install(testhost) + install_host(testhost) QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling) app = QtWidgets.QApplication([]) diff --git a/openpype/pype_commands.py b/openpype/pype_commands.py index c05eece2be..e0c8847040 100644 --- a/openpype/pype_commands.py +++ b/openpype/pype_commands.py @@ -101,7 +101,8 @@ class PypeCommands: RuntimeError: When there is no path to process. """ from openpype.modules import ModulesManager - from openpype import install, uninstall + from openpype.pipeline import install_openpype_plugins + from openpype.api import Logger from openpype.tools.utils.host_tools import show_publish from openpype.tools.utils.lib import qt_app_context @@ -112,7 +113,7 @@ class PypeCommands: log = Logger.get_logger() - install() + install_openpype_plugins() manager = ModulesManager() @@ -294,7 +295,8 @@ class PypeCommands: # Register target and host import pyblish.api import pyblish.util - import avalon.api + + from openpype.pipeline import install_host from openpype.hosts.webpublisher import api as webpublisher log = PypeLogger.get_logger() @@ -315,7 +317,7 @@ class PypeCommands: for target in targets: pyblish.api.register_target(target) - avalon.api.install(webpublisher) + install_host(webpublisher) log.info("Running publish ...") diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 923a1fabdb..23c0909f2b 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -608,14 +608,4 @@ def cli(args): # Store settings api.Session["AVALON_PROJECT"] = project - from avalon import pipeline - - # Find the set config - _config = pipeline.find_config() - if hasattr(_config, "install"): - _config.install() - else: - print("Config `%s` has no function `install`" % - _config.__name__) - show() diff --git a/openpype/tools/utils/host_tools.py b/openpype/tools/utils/host_tools.py index 2d9733ec94..b0c30f6dfb 100644 --- a/openpype/tools/utils/host_tools.py +++ b/openpype/tools/utils/host_tools.py @@ -6,6 +6,7 @@ use singleton approach with global functions (using helper anyway). import os import avalon.api import pyblish.api +from openpype.pipeline import registered_host from .lib import qt_app_context @@ -47,7 +48,7 @@ class HostToolsHelper: Window, validate_host_requirements ) # Host validation - host = avalon.api.registered_host() + host = registered_host() validate_host_requirements(host) workfiles_window = Window(parent=parent) diff --git a/openpype/tools/utils/lib.py b/openpype/tools/utils/lib.py index 422d0f5389..12dd637e6a 100644 --- a/openpype/tools/utils/lib.py +++ b/openpype/tools/utils/lib.py @@ -6,16 +6,17 @@ import collections from Qt import QtWidgets, QtCore, QtGui import qtawesome -import avalon.api - -from openpype.style import get_default_entity_icon_color +from openpype.style import ( + get_default_entity_icon_color, + get_objected_colors, +) +from openpype.resources import get_image_path +from openpype.lib import filter_profiles from openpype.api import ( get_project_settings, Logger ) -from openpype.lib import filter_profiles -from openpype.style import get_objected_colors -from openpype.resources import get_image_path +from openpype.pipeline import registered_host log = Logger.get_logger(__name__) @@ -402,7 +403,7 @@ class FamilyConfigCache: self.family_configs.clear() # Skip if we're not in host context - if not avalon.api.registered_host(): + if not registered_host(): return # Update the icons from the project configuration From f9043329b49573a3c3c89406a25bb266e2ca0106 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:29 +0200 Subject: [PATCH 106/291] removed unused imports --- .../ftrack/event_handlers_user/action_create_folders.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py index d15a865124..0ed12bd03e 100644 --- a/openpype/modules/ftrack/event_handlers_user/action_create_folders.py +++ b/openpype/modules/ftrack/event_handlers_user/action_create_folders.py @@ -1,11 +1,6 @@ import os from openpype_modules.ftrack.lib import BaseAction, statics_icon -from avalon import lib as avalonlib -from openpype.api import ( - Anatomy, - get_project_settings -) -from openpype.lib import ApplicationManager +from openpype.api import Anatomy class CreateFolders(BaseAction): From adc27e5186d30ba87c41b1f973a69d225f9f1174 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:41:46 +0200 Subject: [PATCH 107/291] changed how library loader is shown in tray --- openpype/modules/avalon_apps/avalon_app.py | 47 ++++++++++++---------- openpype/tools/libraryloader/app.py | 10 ----- openpype/tools/libraryloader/lib.py | 21 ---------- 3 files changed, 26 insertions(+), 52 deletions(-) delete mode 100644 openpype/tools/libraryloader/lib.py diff --git a/openpype/modules/avalon_apps/avalon_app.py b/openpype/modules/avalon_apps/avalon_app.py index 51a22323f1..1d21de129b 100644 --- a/openpype/modules/avalon_apps/avalon_app.py +++ b/openpype/modules/avalon_apps/avalon_app.py @@ -1,5 +1,5 @@ import os -import openpype + from openpype.modules import OpenPypeModule from openpype_interfaces import ITrayModule @@ -26,7 +26,8 @@ class AvalonModule(OpenPypeModule, ITrayModule): self.avalon_mongo_timeout = avalon_mongo_timeout # Tray attributes - self.libraryloader = None + self._library_loader_imported = None + self._library_loader_window = None self.rest_api_obj = None def get_global_environments(self): @@ -41,21 +42,11 @@ class AvalonModule(OpenPypeModule, ITrayModule): def tray_init(self): # Add library tool + self._library_loader_imported = False try: - from Qt import QtCore from openpype.tools.libraryloader import LibraryLoaderWindow - libraryloader = LibraryLoaderWindow( - show_projects=True, - show_libraries=True - ) - # Remove always on top flag for tray - window_flags = libraryloader.windowFlags() - if window_flags | QtCore.Qt.WindowStaysOnTopHint: - window_flags ^= QtCore.Qt.WindowStaysOnTopHint - libraryloader.setWindowFlags(window_flags) - self.libraryloader = libraryloader - + self._library_loader_imported = True except Exception: self.log.warning( "Couldn't load Library loader tool for tray.", @@ -64,7 +55,7 @@ class AvalonModule(OpenPypeModule, ITrayModule): # Definition of Tray menu def tray_menu(self, tray_menu): - if self.libraryloader is None: + if not self._library_loader_imported: return from Qt import QtWidgets @@ -84,17 +75,31 @@ class AvalonModule(OpenPypeModule, ITrayModule): return def show_library_loader(self): - if self.libraryloader is None: - return + if self._library_loader_window is None: + from Qt import QtCore + from openpype.tools.libraryloader import LibraryLoaderWindow + from openpype.pipeline import install_openpype_plugins - self.libraryloader.show() + libraryloader = LibraryLoaderWindow( + show_projects=True, + show_libraries=True + ) + # Remove always on top flag for tray + window_flags = libraryloader.windowFlags() + if window_flags | QtCore.Qt.WindowStaysOnTopHint: + window_flags ^= QtCore.Qt.WindowStaysOnTopHint + libraryloader.setWindowFlags(window_flags) + self._library_loader_window = libraryloader + + install_openpype_plugins() + + self._library_loader_window.show() # Raise and activate the window # for MacOS - self.libraryloader.raise_() + self._library_loader_window.raise_() # for Windows - self.libraryloader.activateWindow() - self.libraryloader.refresh() + self._library_loader_window.activateWindow() # Webserver module implementation def webserver_initialization(self, server_manager): diff --git a/openpype/tools/libraryloader/app.py b/openpype/tools/libraryloader/app.py index b73b415128..328e16205c 100644 --- a/openpype/tools/libraryloader/app.py +++ b/openpype/tools/libraryloader/app.py @@ -16,8 +16,6 @@ from openpype.tools.utils.assets_widget import MultiSelectAssetsWidget from openpype.modules import ModulesManager -from . import lib - module = sys.modules[__name__] module.window = None @@ -260,14 +258,6 @@ class LibraryLoaderWindow(QtWidgets.QDialog): self.dbcon.Session["AVALON_PROJECT"] = project_name - _config = lib.find_config() - if hasattr(_config, "install"): - _config.install() - else: - print( - "Config `%s` has no function `install`" % _config.__name__ - ) - self._subsets_widget.on_project_change(project_name) if self._repres_widget: self._repres_widget.on_project_change(project_name) diff --git a/openpype/tools/libraryloader/lib.py b/openpype/tools/libraryloader/lib.py deleted file mode 100644 index 182b48893a..0000000000 --- a/openpype/tools/libraryloader/lib.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import importlib -import logging - -log = logging.getLogger(__name__) - - -# `find_config` from `pipeline` -def find_config(): - log.info("Finding configuration for project..") - - config = os.environ["AVALON_CONFIG"] - - if not config: - raise EnvironmentError( - "No configuration found in " - "the project nor environment" - ) - - log.info("Found %s, loading.." % config) - return importlib.import_module(config) From 4364cd55c39e686348818ff0ea1b4de49631e396 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 1 Apr 2022 19:42:07 +0200 Subject: [PATCH 108/291] cleaned up openpype init file --- openpype/__init__.py | 97 -------------------------------------------- 1 file changed, 97 deletions(-) diff --git a/openpype/__init__.py b/openpype/__init__.py index 7fc7e63e61..810664707a 100644 --- a/openpype/__init__.py +++ b/openpype/__init__.py @@ -1,102 +1,5 @@ -# -*- coding: utf-8 -*- -"""Pype module.""" import os -import platform -import logging - -from .settings import get_project_settings -from .lib import ( - Anatomy, - filter_pyblish_plugins, - change_timer_to_current_context, - register_event_callback, -) - -log = logging.getLogger(__name__) PACKAGE_DIR = os.path.dirname(os.path.abspath(__file__)) PLUGINS_DIR = os.path.join(PACKAGE_DIR, "plugins") - -# Global plugin paths -PUBLISH_PATH = os.path.join(PLUGINS_DIR, "publish") -LOAD_PATH = os.path.join(PLUGINS_DIR, "load") - - -def install(): - """Install OpenPype to Avalon.""" - import avalon.api - import pyblish.api - from pyblish.lib import MessageHandler - from openpype.modules import load_modules - from openpype.pipeline import ( - register_loader_plugin_path, - register_inventory_action, - register_creator_plugin_path, - ) - - # Make sure modules are loaded - load_modules() - - def modified_emit(obj, record): - """Method replacing `emit` in Pyblish's MessageHandler.""" - record.msg = record.getMessage() - obj.records.append(record) - - MessageHandler.emit = modified_emit - - log.info("Registering global plug-ins..") - pyblish.api.register_plugin_path(PUBLISH_PATH) - pyblish.api.register_discovery_filter(filter_pyblish_plugins) - register_loader_plugin_path(LOAD_PATH) - - project_name = os.environ.get("AVALON_PROJECT") - - # Register studio specific plugins - if project_name: - anatomy = Anatomy(project_name) - anatomy.set_root_environments() - avalon.api.register_root(anatomy.roots) - - project_settings = get_project_settings(project_name) - platform_name = platform.system().lower() - project_plugins = ( - project_settings - .get("global", {}) - .get("project_plugins", {}) - .get(platform_name) - ) or [] - for path in project_plugins: - try: - path = str(path.format(**os.environ)) - except KeyError: - pass - - if not path or not os.path.exists(path): - continue - - pyblish.api.register_plugin_path(path) - register_loader_plugin_path(path) - register_creator_plugin_path(path) - register_inventory_action(path) - - # apply monkey patched discover to original one - log.info("Patching discovery") - - register_event_callback("taskChanged", _on_task_change) - - -def _on_task_change(): - change_timer_to_current_context() - - -def uninstall(): - """Uninstall Pype from Avalon.""" - import pyblish.api - from openpype.pipeline import deregister_loader_plugin_path - - log.info("Deregistering global plug-ins..") - pyblish.api.deregister_plugin_path(PUBLISH_PATH) - pyblish.api.deregister_discovery_filter(filter_pyblish_plugins) - deregister_loader_plugin_path(LOAD_PATH) - log.info("Global plug-ins unregistred") From 8ec4d9c8d4bc203c820910ac3b5bd879cfa4b210 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Apr 2022 10:59:43 +0200 Subject: [PATCH 109/291] remove irrelevant comment --- openpype/hosts/celaction/api/cli.py | 1 - 1 file changed, 1 deletion(-) diff --git a/openpype/hosts/celaction/api/cli.py b/openpype/hosts/celaction/api/cli.py index 85e210f21a..ef73c7457a 100644 --- a/openpype/hosts/celaction/api/cli.py +++ b/openpype/hosts/celaction/api/cli.py @@ -71,7 +71,6 @@ def main(): _prepare_publish_environments() # Registers pype's Global pyblish plugins - # - use fake host install_openpype_plugins() if os.path.exists(PUBLISH_PATH): From c8a886d6ce575f1cedccecd1429876746bbaf0a1 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Apr 2022 17:52:23 +0200 Subject: [PATCH 110/291] added install_openpype_plugins into load cli --- openpype/tools/loader/app.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index 23c0909f2b..ab57f63c38 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -5,6 +5,7 @@ from avalon import api, io from openpype import style from openpype.lib import register_event_callback +from openpype.pipeline import install_openpype_plugins from openpype.tools.utils import ( lib, PlaceholderLineEdit @@ -608,4 +609,6 @@ def cli(args): # Store settings api.Session["AVALON_PROJECT"] = project + install_openpype_plugins() + show() From 578a0469c9a487948494f30a036bc14b8882323d Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Mon, 4 Apr 2022 17:57:27 +0200 Subject: [PATCH 111/291] pass project name to plugin install --- openpype/tools/loader/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/loader/app.py b/openpype/tools/loader/app.py index ab57f63c38..fad284d82b 100644 --- a/openpype/tools/loader/app.py +++ b/openpype/tools/loader/app.py @@ -609,6 +609,6 @@ def cli(args): # Store settings api.Session["AVALON_PROJECT"] = project - install_openpype_plugins() + install_openpype_plugins(project) show() From b16b1ee5c48df8438cbe716561df437f941e24c1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Apr 2022 16:39:51 +0200 Subject: [PATCH 112/291] OP-2766 - fix broken merge --- openpype/hosts/photoshop/api/pipeline.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/hosts/photoshop/api/pipeline.py b/openpype/hosts/photoshop/api/pipeline.py index 54db09be2d..2e2717d420 100644 --- a/openpype/hosts/photoshop/api/pipeline.py +++ b/openpype/hosts/photoshop/api/pipeline.py @@ -78,8 +78,7 @@ def install(): pyblish.api.register_plugin_path(PUBLISH_PATH) register_loader_plugin_path(LOAD_PATH) - avalon.api.register_plugin_path(LegacyCreator, CREATE_PATH) - avalon.api.register_plugin_path(BaseCreator, CREATE_PATH) + register_creator_plugin_path(CREATE_PATH) log.info(PUBLISH_PATH) pyblish.api.register_callback( From 43a6863dc534ab514a91a9ade561c9c82e87f277 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 5 Apr 2022 16:40:16 +0200 Subject: [PATCH 113/291] OP-2766 - added documentation and resources for New Publisher --- website/docs/artist_hosts_photoshop.md | 64 ++++++++++++++++++ ...rtist_photoshop_new_publisher_instance.png | Bin 0 -> 21366 bytes ...otoshop_new_publisher_instance_created.png | Bin 0 -> 27811 bytes ...photoshop_new_publisher_publish_failed.png | Bin 0 -> 27081 bytes ...rtist_photoshop_new_publisher_workfile.png | Bin 0 -> 22231 bytes .../docs/assets/experimental_tools_menu.png | Bin 0 -> 9307 bytes .../assets/experimental_tools_settings.png | Bin 0 -> 8543 bytes 7 files changed, 64 insertions(+) create mode 100644 website/docs/assets/artist_photoshop_new_publisher_instance.png create mode 100644 website/docs/assets/artist_photoshop_new_publisher_instance_created.png create mode 100644 website/docs/assets/artist_photoshop_new_publisher_publish_failed.png create mode 100644 website/docs/assets/artist_photoshop_new_publisher_workfile.png create mode 100644 website/docs/assets/experimental_tools_menu.png create mode 100644 website/docs/assets/experimental_tools_settings.png diff --git a/website/docs/artist_hosts_photoshop.md b/website/docs/artist_hosts_photoshop.md index a140170c49..36670054ee 100644 --- a/website/docs/artist_hosts_photoshop.md +++ b/website/docs/artist_hosts_photoshop.md @@ -111,3 +111,67 @@ You can switch to a previous version of the image or update to the latest. ![Loader](assets/photoshop_manage_switch.gif) ![Loader](assets/photoshop_manage_update.gif) + + +### New Publisher + +All previous screenshot came from regular [pyblish](https://pyblish.com/) process, there is also a different UI available. This process extends existing implementation and adds new functionalities. + +To test this in Photoshop, the artist needs first to enable experimental `New publisher` in Settings. (Tray > Settings > Experimental tools) +![Settings](assets/experimental_tools_settings.png) + +New dialog opens after clicking on `Experimental tools` button in Openpype extension menu. +![Menu](assets/experimental_tools_menu.png) + +After you click on this button, this dialog will show up. + +![Menu](assets/artist_photoshop_new_publisher_workfile.png) + +You can see the first instance, called `workfileYourTaskName`. (Name depends on studio naming convention for Photoshop's workfiles.). This instance is so called "automatic", +it was created without instigation by the artist. You shouldn't delete this instance as it might hold necessary values for future publishing, but you can choose to skip it +from publishing (by toggling the pill button inside of the rectangular object denoting instance). + +New publisher allows publishing into different context, just click on a workfile instance, update `Variant`, `Asset` or `Task` in the form in the middle and don't forget to click on the 'Confirm' button. + +Similarly to the old publishing approach, you need to create instances for everything you want to publish. You will initiate by clicking on the '+' sign in the bottom left corner. + +![Instance creator](assets/artist_photoshop_new_publisher_instance.png) + +In this dialog you can select the family for the published layer or group. Currently only 'image' is implemented. + +On right hand side you can see creator attributes: +- `Create only for selected` - mimics `Use selected` option of regular publish +- `Create separate instance for each selected` - if separate instance should be created for each layer if multiple selected + +![Instance created](assets/artist_photoshop_new_publisher_instance_created.png) + +Here you can see a newly created instance of image family. (Name depends on studio naming convention for image family.) You can disable instance from publishing in the same fashion as a workfile instance. +You could also decide delete instance by selecting it and clicking on a trashcan icon (next to plus button on left button) + +Buttons on the bottom right are for: +- `Refresh publishing` - set publishing process to starting position - useful if previous publish failed, or you changed configuration of a publish +- `Stop/pause publishing` - if you would like to pause publishing process at any time +- `Validate` - if you would like to run only collecting and validating phases (nothing will be published yet) +- `Publish` - standard way how to kick off full publishing process + +In the unfortunate case of some error during publishing, you would receive this kind of error dialog. + +![Publish failed](assets/artist_photoshop_new_publisher_publish_failed.png) + +In this case there is an issue that you are publishing two or more instances with the same subset name ('imageMaing'). If the error is recoverable by the artist, you should +see helpful information in a `How to repair?` section or fix it automatically by clicking on a 'Wrench' button on the right if present. + +If you would like to ask for help admin or support, you could use any of the three buttons on bottom left: +- `Copy report` - stash full publishing log to a clipboard +- `Export and save report` - save log into a file for sending it via mail or any communication tool +- `Show details` - switches into a more detailed list of published instances and plugins. Similar to the old pyblish list. + +If you are able to fix the workfile yourself, use the first button on the right to set the UI to initial state before publish. (Click the `Publish` button to start again.) + +New publishing process should be backward compatible, eg. if you have a workfile with instances created in the previous publishing approach, they will be translated automatically and +could be used right away. + +If you would create instances in a new publisher, you cannot use them in the old approach though! + +If you would hit on unexpected behaviour with old instances, contact support first, then you could try some steps to recover your publish. Delete instances in New publisher UI, or try `Subset manager` in the extension menu. +Nuclear option is to purge workfile metadata in `File > File Info > Origin > Headline`. This is only for most determined daredevils though! diff --git a/website/docs/assets/artist_photoshop_new_publisher_instance.png b/website/docs/assets/artist_photoshop_new_publisher_instance.png new file mode 100644 index 0000000000000000000000000000000000000000..723a032c94488abf6ee60e11ab86b178e1cd5ade GIT binary patch literal 21366 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 literal 0 HcmV?d00001 diff --git a/website/docs/assets/artist_photoshop_new_publisher_publish_failed.png b/website/docs/assets/artist_photoshop_new_publisher_publish_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..e34497b77da37b31ebbf26882242ed73040eba64 GIT binary patch literal 27081 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